diff --git a/Config/DefaultPHYUI.ini b/Config/DefaultPHYUI.ini index b9d2d28..b5c4877 100644 --- a/Config/DefaultPHYUI.ini +++ b/Config/DefaultPHYUI.ini @@ -4,6 +4,7 @@ bUseGenericUISystem=True bOpenMenusWithGameplayPause=False RootLayoutClass="/Script/PHY.PHYGameUILayout" DefaultHUDWidgetClass="/Script/PHY.PHYHUDStatusWidget" +AttributeMenuWidgetClass="/Script/PHY.PHYAttributeMenuWidget" GameplayLayerTag=(TagName="UI.Layer.Game") HUDLayerTag=(TagName="UI.Layer.HUD") MenuLayerTag=(TagName="UI.Layer.Menu") diff --git a/Source/PHY/Private/Game/PHYHUD.cpp b/Source/PHY/Private/Game/PHYHUD.cpp index 2df664e..07d089c 100644 --- a/Source/PHY/Private/Game/PHYHUD.cpp +++ b/Source/PHY/Private/Game/PHYHUD.cpp @@ -11,6 +11,7 @@ #include "UI/GUIS_GameUILayout.h" #include "UI/GUIS_GameUIPolicy.h" #include "UI/GUIS_GameUISubsystem.h" +#include "UI/PHYAttributeMenuWidget.h" #include "UI/PHYHUDStatusWidget.h" #include "UI/PHYPlayerUIContext.h" @@ -40,6 +41,38 @@ void APHYHUD::InitializeHUDForPlayer(APlayerController* OwningPlayerController) } } +UCommonActivatableWidget* APHYHUD::OpenAttributeMenu(APlayerController* OwningPlayerController) +{ + APlayerController* TargetPlayerController = OwningPlayerController ? OwningPlayerController : GetOwningPlayerController(); + if (!TargetPlayerController) + { + return nullptr; + } + + if (!bHUDInitialized) + { + InitializeHUDForPlayer(TargetPlayerController); + } + + const UPHYUISettings* UISettings = GetDefault(); + TSubclassOf AttributeMenuClass = UPHYAttributeMenuWidget::StaticClass(); + if (UISettings && !UISettings->AttributeMenuWidgetClass.IsNull()) + { + if (UClass* LoadedAttributeMenuClass = UISettings->AttributeMenuWidgetClass.LoadSynchronous()) + { + AttributeMenuClass = LoadedAttributeMenuClass; + } + } + + UCommonActivatableWidget* PushedWidget = PushMenuWidget(TargetPlayerController, AttributeMenuClass); + if (UPHYAttributeMenuWidget* AttributeMenuWidget = Cast(PushedWidget)) + { + AttributeMenuWidget->InitializeFromPlayer(TargetPlayerController); + } + + return PushedWidget; +} + bool APHYHUD::InitializeGenericUIForPlayer(APlayerController* OwningPlayerController) { if (!OwningPlayerController || !OwningPlayerController->IsLocalController()) @@ -117,3 +150,28 @@ UCommonActivatableWidget* APHYHUD::PushDefaultHUDWidget(APlayerController* Ownin return PushedWidget; } + +UCommonActivatableWidget* APHYHUD::PushMenuWidget(APlayerController* OwningPlayerController, TSubclassOf WidgetClass) +{ + if (!OwningPlayerController || !WidgetClass) + { + return nullptr; + } + + const UPHYUISettings* UISettings = GetDefault(); + ULocalPlayer* LocalPlayer = OwningPlayerController->GetLocalPlayer(); + if (!LocalPlayer) + { + return nullptr; + } + + UGUIS_GameUIPolicy* UIPolicy = UGUIS_GameUIPolicy::GetGameUIPolicy(OwningPlayerController); + UGUIS_GameUILayout* RootLayout = UIPolicy ? UIPolicy->GetRootLayout(LocalPlayer) : nullptr; + if (!RootLayout) + { + return nullptr; + } + + const FGameplayTag MenuLayerTag = (UISettings && UISettings->MenuLayerTag.IsValid()) ? UISettings->MenuLayerTag : PHYGameplayTags::UI_Layer_Menu; + return RootLayout->PushWidgetToLayerStack(MenuLayerTag, WidgetClass); +} diff --git a/Source/PHY/Private/UI/PHYAttributeGroupWidget.cpp b/Source/PHY/Private/UI/PHYAttributeGroupWidget.cpp new file mode 100644 index 0000000..25f4bce --- /dev/null +++ b/Source/PHY/Private/UI/PHYAttributeGroupWidget.cpp @@ -0,0 +1,106 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "UI/PHYAttributeGroupWidget.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAttributeGroupWidget) + +#include "Blueprint/WidgetTree.h" +#include "Components/Border.h" +#include "Components/TextBlock.h" +#include "Components/VerticalBox.h" +#include "Components/VerticalBoxSlot.h" +#include "Fonts/SlateFontInfo.h" +#include "Styling/CoreStyle.h" +#include "UI/PHYAttributeRowWidget.h" +#include "Widgets/SWidget.h" + +UPHYAttributeGroupWidget::UPHYAttributeGroupWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +TSharedRef UPHYAttributeGroupWidget::RebuildWidget() +{ + BuildNativeWidgetTree(); + return Super::RebuildWidget(); +} + +void UPHYAttributeGroupWidget::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + UpdateGroupWidgets(); +} + +void UPHYAttributeGroupWidget::SetGroupTitle(FText InGroupTitle) +{ + GroupTitle = MoveTemp(InGroupTitle); + UpdateGroupWidgets(); +} + +void UPHYAttributeGroupWidget::ClearRows() +{ + if (RowsBox) + { + RowsBox->ClearChildren(); + } +} + +UPHYAttributeRowWidget* UPHYAttributeGroupWidget::AddRow(FText Label, FText Value, const bool bCanUpgrade) +{ + BuildNativeWidgetTree(); + + if (!RowsBox || !WidgetTree) + { + return nullptr; + } + + UPHYAttributeRowWidget* RowWidget = WidgetTree->ConstructWidget(UPHYAttributeRowWidget::StaticClass()); + RowWidget->SetRow(MoveTemp(Label), MoveTemp(Value), bCanUpgrade); + if (UVerticalBoxSlot* RowSlot = RowsBox->AddChildToVerticalBox(RowWidget)) + { + RowSlot->SetPadding(FMargin(0.0f, 1.0f)); + } + return RowWidget; +} + +void UPHYAttributeGroupWidget::BuildNativeWidgetTree() +{ + if (!WidgetTree) + { + WidgetTree = NewObject(this, TEXT("WidgetTree"), RF_Transient); + } + + if (WidgetTree->RootWidget && TitleText && RowsBox) + { + return; + } + + UBorder* RootBorder = WidgetTree->ConstructWidget(UBorder::StaticClass(), TEXT("RootBorder")); + RootBorder->SetBrushColor(FLinearColor(0.98f, 0.94f, 0.84f, 0.58f)); + RootBorder->SetPadding(FMargin(14.0f, 10.0f)); + WidgetTree->RootWidget = RootBorder; + + UVerticalBox* RootBox = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("RootBox")); + RootBorder->SetContent(RootBox); + + TitleText = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), TEXT("TitleText")); + TitleText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 17)); + TitleText->SetColorAndOpacity(FSlateColor(FLinearColor(0.56f, 0.18f, 0.10f, 1.0f))); + if (UVerticalBoxSlot* TitleSlot = RootBox->AddChildToVerticalBox(TitleText)) + { + TitleSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 6.0f)); + } + + RowsBox = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("RowsBox")); + RootBox->AddChildToVerticalBox(RowsBox); + + UpdateGroupWidgets(); +} + +void UPHYAttributeGroupWidget::UpdateGroupWidgets() +{ + if (TitleText) + { + TitleText->SetText(GroupTitle); + } +} diff --git a/Source/PHY/Private/UI/PHYAttributeMenuWidget.cpp b/Source/PHY/Private/UI/PHYAttributeMenuWidget.cpp new file mode 100644 index 0000000..1d2e610 --- /dev/null +++ b/Source/PHY/Private/UI/PHYAttributeMenuWidget.cpp @@ -0,0 +1,460 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "UI/PHYAttributeMenuWidget.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAttributeMenuWidget) + +#include "AbilitySystemComponent.h" +#include "AbilitySystem/Attributes/PHYCombatAttributeSet.h" +#include "AbilitySystem/Attributes/PHYCoreAttributeSet.h" +#include "AbilitySystem/Attributes/PHYElementAttributeSet.h" +#include "Blueprint/WidgetTree.h" +#include "Components/Border.h" +#include "Components/Button.h" +#include "Components/HorizontalBox.h" +#include "Components/HorizontalBoxSlot.h" +#include "Components/SafeZone.h" +#include "Components/SizeBox.h" +#include "Components/TextBlock.h" +#include "Components/VerticalBox.h" +#include "Components/VerticalBoxSlot.h" +#include "Components/WidgetSwitcher.h" +#include "Fonts/SlateFontInfo.h" +#include "GameFramework/PlayerController.h" +#include "Player/PHYPlayerState.h" +#include "Styling/CoreStyle.h" +#include "UI/PHYAttributeOverviewPageWidget.h" +#include "UI/PHYAttributeSummaryWidget.h" +#include "UI/PHYCombatDerivedAttributePageWidget.h" +#include "UI/PHYElementDerivedAttributePageWidget.h" +#include "Widgets/SWidget.h" + +namespace +{ + TArray MakeTrackedAttributes() + { + return { + UPHYCoreAttributeSet::GetStrengthAttribute(), + UPHYCoreAttributeSet::GetDexterityAttribute(), + UPHYCoreAttributeSet::GetVitalityAttribute(), + UPHYCoreAttributeSet::GetIntelligenceAttribute(), + UPHYCoreAttributeSet::GetSpiritAttribute(), + UPHYCoreAttributeSet::GetPerceptionAttribute(), + UPHYCombatAttributeSet::GetHealthAttribute(), + UPHYCombatAttributeSet::GetMaxHealthAttribute(), + UPHYCombatAttributeSet::GetManaAttribute(), + UPHYCombatAttributeSet::GetMaxManaAttribute(), + UPHYCombatAttributeSet::GetStaminaAttribute(), + UPHYCombatAttributeSet::GetMaxStaminaAttribute(), + UPHYCombatAttributeSet::GetPhysicalAttackPowerAttribute(), + UPHYCombatAttributeSet::GetSpellPowerAttribute(), + UPHYCombatAttributeSet::GetArmorAttribute(), + UPHYCombatAttributeSet::GetMagicResistanceAttribute(), + UPHYCombatAttributeSet::GetAccuracyAttribute(), + UPHYCombatAttributeSet::GetEvasionAttribute(), + UPHYCombatAttributeSet::GetCriticalChanceAttribute(), + UPHYCombatAttributeSet::GetCriticalDamageAttribute(), + UPHYCombatAttributeSet::GetAttackSpeedAttribute(), + UPHYCombatAttributeSet::GetCooldownReductionAttribute(), + UPHYCombatAttributeSet::GetBlockPowerAttribute(), + UPHYCombatAttributeSet::GetGuardBreakPowerAttribute(), + UPHYCombatAttributeSet::GetPoiseAttribute(), + UPHYCombatAttributeSet::GetPoiseDamageAttribute(), + UPHYElementAttributeSet::GetFireDamageBonusAttribute(), + UPHYElementAttributeSet::GetWaterDamageBonusAttribute(), + UPHYElementAttributeSet::GetIceDamageBonusAttribute(), + UPHYElementAttributeSet::GetLightningDamageBonusAttribute(), + UPHYElementAttributeSet::GetEarthDamageBonusAttribute(), + UPHYElementAttributeSet::GetWindDamageBonusAttribute(), + UPHYElementAttributeSet::GetLightDamageBonusAttribute(), + UPHYElementAttributeSet::GetDarkDamageBonusAttribute(), + UPHYElementAttributeSet::GetFireResistanceAttribute(), + UPHYElementAttributeSet::GetWaterResistanceAttribute(), + UPHYElementAttributeSet::GetIceResistanceAttribute(), + UPHYElementAttributeSet::GetLightningResistanceAttribute(), + UPHYElementAttributeSet::GetEarthResistanceAttribute(), + UPHYElementAttributeSet::GetWindResistanceAttribute(), + UPHYElementAttributeSet::GetLightResistanceAttribute(), + UPHYElementAttributeSet::GetDarkResistanceAttribute(), + UPHYElementAttributeSet::GetElementPenetrationAttribute() + }; + } +} + +UPHYAttributeMenuWidget::UPHYAttributeMenuWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + InputConfig = EGUIS_ActivatableWidgetInputMode::Menu; + SetIsBackHandler(true); +} + +TSharedRef UPHYAttributeMenuWidget::RebuildWidget() +{ + BuildNativeWidgetTree(); + return Super::RebuildWidget(); +} + +void UPHYAttributeMenuWidget::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + RefreshAttributePages(); + UpdateTabVisuals(); +} + +void UPHYAttributeMenuWidget::InitializeFromPlayer(APlayerController* InPlayerController) +{ + if (!InPlayerController) + { + BoundPlayerController.Reset(); + UnbindFromPlayerState(); + UnbindFromAbilitySystem(); + RefreshAttributePages(); + return; + } + + BoundPlayerController = InPlayerController; + RefreshFromBoundSources(); +} + +void UPHYAttributeMenuWidget::RefreshFromBoundSources() +{ + APlayerController* PlayerController = BoundPlayerController.Get(); + if (!PlayerController) + { + UnbindFromPlayerState(); + UnbindFromAbilitySystem(); + RefreshAttributePages(); + return; + } + + APHYPlayerState* PHYPlayerState = PlayerController->GetPlayerState(); + BindToPlayerState(PHYPlayerState); + BindToAbilitySystem(PHYPlayerState ? PHYPlayerState->GetAbilitySystemComponent() : nullptr); + RefreshAttributePages(); +} + +void UPHYAttributeMenuWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + if (!BoundPlayerController.IsValid()) + { + InitializeFromPlayer(GetOwningPlayer()); + } + else + { + RefreshFromBoundSources(); + } +} + +void UPHYAttributeMenuWidget::NativeDestruct() +{ + UnbindFromPlayerState(); + UnbindFromAbilitySystem(); + BoundPlayerController.Reset(); + + Super::NativeDestruct(); +} + +void UPHYAttributeMenuWidget::NativeTick(const FGeometry& MyGeometry, const float InDeltaTime) +{ + Super::NativeTick(MyGeometry, InDeltaTime); + + APlayerController* PlayerController = BoundPlayerController.Get(); + if (!PlayerController) + { + return; + } + + APHYPlayerState* CurrentPlayerState = PlayerController->GetPlayerState(); + if (CurrentPlayerState != BoundPlayerState.Get() || !BoundAbilitySystemComponent.IsValid()) + { + RefreshFromBoundSources(); + } +} + +void UPHYAttributeMenuWidget::BuildNativeWidgetTree() +{ + if (!WidgetTree) + { + WidgetTree = NewObject(this, TEXT("WidgetTree"), RF_Transient); + } + + if (WidgetTree->RootWidget && SummaryWidget && PageSwitcher && OverviewPage && CombatPage && ElementPage) + { + return; + } + + USafeZone* RootSafeZone = WidgetTree->ConstructWidget(USafeZone::StaticClass(), TEXT("RootSafeZone")); + WidgetTree->RootWidget = RootSafeZone; + + UBorder* RootBorder = WidgetTree->ConstructWidget(UBorder::StaticClass(), TEXT("RootBorder")); + RootBorder->SetBrushColor(FLinearColor(0.83f, 0.78f, 0.64f, 0.94f)); + RootBorder->SetPadding(FMargin(28.0f)); + RootSafeZone->SetContent(RootBorder); + + UHorizontalBox* MainBox = WidgetTree->ConstructWidget(UHorizontalBox::StaticClass(), TEXT("MainBox")); + RootBorder->SetContent(MainBox); + + SummaryWidget = WidgetTree->ConstructWidget(UPHYAttributeSummaryWidget::StaticClass(), TEXT("SummaryWidget")); + USizeBox* SummarySizeBox = WidgetTree->ConstructWidget(USizeBox::StaticClass(), TEXT("SummarySizeBox")); + SummarySizeBox->SetWidthOverride(360.0f); + SummarySizeBox->AddChild(SummaryWidget); + if (UHorizontalBoxSlot* SummarySlot = MainBox->AddChildToHorizontalBox(SummarySizeBox)) + { + SummarySlot->SetSize(FSlateChildSize(ESlateSizeRule::Automatic)); + SummarySlot->SetPadding(FMargin(0.0f, 0.0f, 18.0f, 0.0f)); + } + + UBorder* PageBorder = WidgetTree->ConstructWidget(UBorder::StaticClass(), TEXT("PageBorder")); + PageBorder->SetBrushColor(FLinearColor(0.96f, 0.92f, 0.82f, 0.72f)); + PageBorder->SetPadding(FMargin(22.0f)); + if (UHorizontalBoxSlot* PageSlot = MainBox->AddChildToHorizontalBox(PageBorder)) + { + PageSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); + } + + UVerticalBox* PageBox = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("PageBox")); + PageBorder->SetContent(PageBox); + + UTextBlock* HeaderText = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), TEXT("HeaderText")); + HeaderText->SetText(FText::FromString(TEXT("角色属性"))); + HeaderText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 28)); + HeaderText->SetColorAndOpacity(FSlateColor(FLinearColor(0.35f, 0.13f, 0.06f, 1.0f))); + PageBox->AddChildToVerticalBox(HeaderText); + + UHorizontalBox* TabBox = WidgetTree->ConstructWidget(UHorizontalBox::StaticClass(), TEXT("TabBox")); + if (UVerticalBoxSlot* TabSlot = PageBox->AddChildToVerticalBox(TabBox)) + { + TabSlot->SetPadding(FMargin(0.0f, 12.0f, 0.0f, 16.0f)); + } + + OverviewTabButton = CreateTabButton(TEXT("OverviewTabButton"), FText::FromString(TEXT("总览"))); + CombatTabButton = CreateTabButton(TEXT("CombatTabButton"), FText::FromString(TEXT("战斗衍生"))); + ElementTabButton = CreateTabButton(TEXT("ElementTabButton"), FText::FromString(TEXT("元素衍生"))); + + TabBox->AddChildToHorizontalBox(OverviewTabButton); + if (UHorizontalBoxSlot* CombatTabSlot = TabBox->AddChildToHorizontalBox(CombatTabButton)) + { + CombatTabSlot->SetPadding(FMargin(8.0f, 0.0f, 0.0f, 0.0f)); + } + if (UHorizontalBoxSlot* ElementTabSlot = TabBox->AddChildToHorizontalBox(ElementTabButton)) + { + ElementTabSlot->SetPadding(FMargin(8.0f, 0.0f, 0.0f, 0.0f)); + } + + PageSwitcher = WidgetTree->ConstructWidget(UWidgetSwitcher::StaticClass(), TEXT("PageSwitcher")); + if (UVerticalBoxSlot* SwitcherSlot = PageBox->AddChildToVerticalBox(PageSwitcher)) + { + SwitcherSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); + } + + OverviewPage = WidgetTree->ConstructWidget(UPHYAttributeOverviewPageWidget::StaticClass(), TEXT("OverviewPage")); + CombatPage = WidgetTree->ConstructWidget(UPHYCombatDerivedAttributePageWidget::StaticClass(), TEXT("CombatPage")); + ElementPage = WidgetTree->ConstructWidget(UPHYElementDerivedAttributePageWidget::StaticClass(), TEXT("ElementPage")); + PageSwitcher->AddChild(OverviewPage); + PageSwitcher->AddChild(CombatPage); + PageSwitcher->AddChild(ElementPage); + + OverviewTabButton->OnClicked.AddUniqueDynamic(this, &UPHYAttributeMenuWidget::HandleOverviewTabClicked); + CombatTabButton->OnClicked.AddUniqueDynamic(this, &UPHYAttributeMenuWidget::HandleCombatTabClicked); + ElementTabButton->OnClicked.AddUniqueDynamic(this, &UPHYAttributeMenuWidget::HandleElementTabClicked); + + SetActivePage(ActivePageIndex); + RefreshAttributePages(); +} + +UButton* UPHYAttributeMenuWidget::CreateTabButton(const FName ButtonName, const FText Label) +{ + UButton* TabButton = WidgetTree->ConstructWidget(UButton::StaticClass(), ButtonName); + TabButton->SetBackgroundColor(FLinearColor(0.25f, 0.48f, 0.42f, 0.78f)); + + UTextBlock* TabText = WidgetTree->ConstructWidget(UTextBlock::StaticClass()); + TabText->SetText(Label); + TabText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 16)); + TabText->SetColorAndOpacity(FSlateColor(FLinearColor(0.98f, 0.94f, 0.84f, 1.0f))); + TabButton->SetContent(TabText); + return TabButton; +} + +void UPHYAttributeMenuWidget::SetActivePage(const int32 NewPageIndex) +{ + ActivePageIndex = FMath::Clamp(NewPageIndex, 0, 2); + if (PageSwitcher) + { + PageSwitcher->SetActiveWidgetIndex(ActivePageIndex); + } + UpdateTabVisuals(); +} + +void UPHYAttributeMenuWidget::UpdateTabVisuals() +{ + const FLinearColor ActiveColor(0.68f, 0.21f, 0.12f, 0.92f); + const FLinearColor InactiveColor(0.25f, 0.48f, 0.42f, 0.78f); + + if (OverviewTabButton) + { + OverviewTabButton->SetBackgroundColor(ActivePageIndex == 0 ? ActiveColor : InactiveColor); + } + if (CombatTabButton) + { + CombatTabButton->SetBackgroundColor(ActivePageIndex == 1 ? ActiveColor : InactiveColor); + } + if (ElementTabButton) + { + ElementTabButton->SetBackgroundColor(ActivePageIndex == 2 ? ActiveColor : InactiveColor); + } +} + +void UPHYAttributeMenuWidget::BindToPlayerState(APHYPlayerState* NewPlayerState) +{ + if (BoundPlayerState.Get() == NewPlayerState) + { + return; + } + + UnbindFromPlayerState(); + BoundPlayerState = NewPlayerState; + + if (NewPlayerState) + { + NewPlayerState->OnProgressionChanged.AddUniqueDynamic(this, &UPHYAttributeMenuWidget::HandleProgressionChanged); + } +} + +void UPHYAttributeMenuWidget::UnbindFromPlayerState() +{ + if (APHYPlayerState* PlayerState = BoundPlayerState.Get()) + { + PlayerState->OnProgressionChanged.RemoveDynamic(this, &UPHYAttributeMenuWidget::HandleProgressionChanged); + } + BoundPlayerState.Reset(); +} + +void UPHYAttributeMenuWidget::BindToAbilitySystem(UAbilitySystemComponent* NewAbilitySystemComponent) +{ + if (BoundAbilitySystemComponent.Get() == NewAbilitySystemComponent && AttributeChangedHandles.Num() > 0) + { + return; + } + + UnbindFromAbilitySystem(); + BoundAbilitySystemComponent = NewAbilitySystemComponent; + + if (!NewAbilitySystemComponent) + { + return; + } + + BoundAttributes = MakeTrackedAttributes(); + AttributeChangedHandles.Reserve(BoundAttributes.Num()); + for (const FGameplayAttribute& Attribute : BoundAttributes) + { + AttributeChangedHandles.Add(NewAbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attribute).AddUObject(this, &UPHYAttributeMenuWidget::HandleAttributeChanged)); + } +} + +void UPHYAttributeMenuWidget::UnbindFromAbilitySystem() +{ + if (UAbilitySystemComponent* AbilitySystemComponent = BoundAbilitySystemComponent.Get()) + { + for (int32 Index = 0; Index < BoundAttributes.Num() && Index < AttributeChangedHandles.Num(); ++Index) + { + if (AttributeChangedHandles[Index].IsValid()) + { + AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(BoundAttributes[Index]).Remove(AttributeChangedHandles[Index]); + } + } + } + + AttributeChangedHandles.Reset(); + BoundAttributes.Reset(); + BoundAbilitySystemComponent.Reset(); +} + +void UPHYAttributeMenuWidget::RefreshAttributePages() +{ + if (SummaryWidget) + { + const APHYPlayerState* PlayerState = BoundPlayerState.Get(); + const FString PlayerName = PlayerState ? PlayerState->GetPlayerName() : TEXT("修行者"); + const int32 Level = PlayerState ? PlayerState->GetPlayerLevel() : 1; + const int32 Experience = PlayerState ? PlayerState->GetExperience() : 0; + const int32 ExperienceForNextLevel = PlayerState ? PlayerState->GetExperienceForNextLevel() : 100; + SummaryWidget->SetSummary(FText::FromString(PlayerName), Level, Experience, ExperienceForNextLevel, CalculateDisplayCombatPower()); + SummaryWidget->SetResources( + GetAttributeValue(UPHYCombatAttributeSet::GetHealthAttribute()), + GetAttributeValue(UPHYCombatAttributeSet::GetMaxHealthAttribute()), + GetAttributeValue(UPHYCombatAttributeSet::GetManaAttribute()), + GetAttributeValue(UPHYCombatAttributeSet::GetMaxManaAttribute()), + GetAttributeValue(UPHYCombatAttributeSet::GetStaminaAttribute()), + GetAttributeValue(UPHYCombatAttributeSet::GetMaxStaminaAttribute())); + } + + UAbilitySystemComponent* AbilitySystemComponent = BoundAbilitySystemComponent.Get(); + if (OverviewPage) + { + OverviewPage->RefreshAttributes(AbilitySystemComponent); + } + if (CombatPage) + { + CombatPage->RefreshAttributes(AbilitySystemComponent); + } + if (ElementPage) + { + ElementPage->RefreshAttributes(AbilitySystemComponent); + } +} + +void UPHYAttributeMenuWidget::HandleAttributeChanged(const FOnAttributeChangeData& /*ChangeData*/) +{ + RefreshAttributePages(); +} + +void UPHYAttributeMenuWidget::HandleProgressionChanged(const int32 /*NewLevel*/, const int32 /*NewExperience*/, const int32 /*NewExperienceForNextLevel*/) +{ + RefreshAttributePages(); +} + +void UPHYAttributeMenuWidget::HandleOverviewTabClicked() +{ + SetActivePage(0); +} + +void UPHYAttributeMenuWidget::HandleCombatTabClicked() +{ + SetActivePage(1); +} + +void UPHYAttributeMenuWidget::HandleElementTabClicked() +{ + SetActivePage(2); +} + +float UPHYAttributeMenuWidget::GetAttributeValue(const FGameplayAttribute& Attribute) const +{ + UAbilitySystemComponent* AbilitySystemComponent = BoundAbilitySystemComponent.Get(); + return AbilitySystemComponent ? AbilitySystemComponent->GetNumericAttribute(Attribute) : 0.0f; +} + +int32 UPHYAttributeMenuWidget::CalculateDisplayCombatPower() const +{ + const float OffensivePower = + GetAttributeValue(UPHYCombatAttributeSet::GetPhysicalAttackPowerAttribute()) + + GetAttributeValue(UPHYCombatAttributeSet::GetSpellPowerAttribute()) + + GetAttributeValue(UPHYCombatAttributeSet::GetGuardBreakPowerAttribute()) + + GetAttributeValue(UPHYCombatAttributeSet::GetPoiseDamageAttribute()); + + const float DefensivePower = + GetAttributeValue(UPHYCombatAttributeSet::GetArmorAttribute()) + + GetAttributeValue(UPHYCombatAttributeSet::GetMagicResistanceAttribute()) + + GetAttributeValue(UPHYCombatAttributeSet::GetBlockPowerAttribute()) + + GetAttributeValue(UPHYCombatAttributeSet::GetPoiseAttribute()); + + const float ResourcePower = + GetAttributeValue(UPHYCombatAttributeSet::GetMaxHealthAttribute()) * 0.20f + + GetAttributeValue(UPHYCombatAttributeSet::GetMaxManaAttribute()) * 0.10f + + GetAttributeValue(UPHYCombatAttributeSet::GetMaxStaminaAttribute()) * 0.10f; + + return FMath::RoundToInt(OffensivePower + DefensivePower + ResourcePower); +} diff --git a/Source/PHY/Private/UI/PHYAttributeOverviewPageWidget.cpp b/Source/PHY/Private/UI/PHYAttributeOverviewPageWidget.cpp new file mode 100644 index 0000000..0242d0a --- /dev/null +++ b/Source/PHY/Private/UI/PHYAttributeOverviewPageWidget.cpp @@ -0,0 +1,55 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "UI/PHYAttributeOverviewPageWidget.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAttributeOverviewPageWidget) + +#include "AbilitySystemComponent.h" +#include "AbilitySystem/Attributes/PHYCombatAttributeSet.h" +#include "AbilitySystem/Attributes/PHYCoreAttributeSet.h" +#include "UI/PHYAttributeGroupWidget.h" + +namespace +{ + float GetAttributeValue(const UAbilitySystemComponent* AbilitySystemComponent, const FGameplayAttribute& Attribute) + { + return AbilitySystemComponent ? AbilitySystemComponent->GetNumericAttribute(Attribute) : 0.0f; + } + + FText MakeNumberText(const float Value) + { + return FText::FromString(FString::Printf(TEXT("%.0f"), Value)); + } + + FText MakePairText(const float CurrentValue, const float MaxValue) + { + return FText::FromString(FString::Printf(TEXT("%.0f / %.0f"), FMath::Max(0.0f, CurrentValue), FMath::Max(0.0f, MaxValue))); + } + + void AddAttributeRow(UPHYAttributeGroupWidget* GroupWidget, const TCHAR* Label, const FText& Value, const bool bCanUpgrade = false) + { + if (GroupWidget) + { + GroupWidget->AddRow(FText::FromString(Label), Value, bCanUpgrade); + } + } +} + +void UPHYAttributeOverviewPageWidget::RefreshAttributes(UAbilitySystemComponent* AbilitySystemComponent) +{ + SetPageTitle(FText::FromString(TEXT("属性总览"))); + ClearGroups(); + + UPHYAttributeGroupWidget* CoreGroup = AddGroup(FText::FromString(TEXT("核心属性"))); + AddAttributeRow(CoreGroup, TEXT("力量"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCoreAttributeSet::GetStrengthAttribute())), true); + AddAttributeRow(CoreGroup, TEXT("敏捷"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCoreAttributeSet::GetDexterityAttribute())), true); + AddAttributeRow(CoreGroup, TEXT("体质"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCoreAttributeSet::GetVitalityAttribute())), true); + AddAttributeRow(CoreGroup, TEXT("智力"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCoreAttributeSet::GetIntelligenceAttribute())), true); + AddAttributeRow(CoreGroup, TEXT("精神"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCoreAttributeSet::GetSpiritAttribute())), true); + AddAttributeRow(CoreGroup, TEXT("感知"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCoreAttributeSet::GetPerceptionAttribute())), true); + + UPHYAttributeGroupWidget* ResourceGroup = AddGroup(FText::FromString(TEXT("战斗资源"))); + AddAttributeRow(ResourceGroup, TEXT("生命值"), MakePairText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetHealthAttribute()), GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxHealthAttribute()))); + AddAttributeRow(ResourceGroup, TEXT("法力值"), MakePairText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetManaAttribute()), GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxManaAttribute()))); + AddAttributeRow(ResourceGroup, TEXT("耐力值"), MakePairText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetStaminaAttribute()), GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxStaminaAttribute()))); +} diff --git a/Source/PHY/Private/UI/PHYAttributePageWidget.cpp b/Source/PHY/Private/UI/PHYAttributePageWidget.cpp new file mode 100644 index 0000000..ffb3c90 --- /dev/null +++ b/Source/PHY/Private/UI/PHYAttributePageWidget.cpp @@ -0,0 +1,111 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "UI/PHYAttributePageWidget.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAttributePageWidget) + +#include "Blueprint/WidgetTree.h" +#include "Components/ScrollBox.h" +#include "Components/ScrollBoxSlot.h" +#include "Components/TextBlock.h" +#include "Components/VerticalBox.h" +#include "Components/VerticalBoxSlot.h" +#include "Fonts/SlateFontInfo.h" +#include "Styling/CoreStyle.h" +#include "UI/PHYAttributeGroupWidget.h" +#include "Widgets/SWidget.h" + +UPHYAttributePageWidget::UPHYAttributePageWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +TSharedRef UPHYAttributePageWidget::RebuildWidget() +{ + BuildNativeWidgetTree(); + return Super::RebuildWidget(); +} + +void UPHYAttributePageWidget::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + UpdatePageWidgets(); +} + +void UPHYAttributePageWidget::SetPageTitle(FText InPageTitle) +{ + PageTitle = MoveTemp(InPageTitle); + UpdatePageWidgets(); +} + +void UPHYAttributePageWidget::ClearGroups() +{ + if (GroupsBox) + { + GroupsBox->ClearChildren(); + } +} + +UPHYAttributeGroupWidget* UPHYAttributePageWidget::AddGroup(FText GroupTitle) +{ + BuildNativeWidgetTree(); + + if (!GroupsBox || !WidgetTree) + { + return nullptr; + } + + UPHYAttributeGroupWidget* GroupWidget = WidgetTree->ConstructWidget(UPHYAttributeGroupWidget::StaticClass()); + GroupWidget->SetGroupTitle(MoveTemp(GroupTitle)); + if (UVerticalBoxSlot* GroupSlot = GroupsBox->AddChildToVerticalBox(GroupWidget)) + { + GroupSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 12.0f)); + } + return GroupWidget; +} + +void UPHYAttributePageWidget::BuildNativeWidgetTree() +{ + if (!WidgetTree) + { + WidgetTree = NewObject(this, TEXT("WidgetTree"), RF_Transient); + } + + if (WidgetTree->RootWidget && TitleText && GroupsScrollBox && GroupsBox) + { + return; + } + + UVerticalBox* RootBox = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("RootBox")); + WidgetTree->RootWidget = RootBox; + + TitleText = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), TEXT("TitleText")); + TitleText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 22)); + TitleText->SetColorAndOpacity(FSlateColor(FLinearColor(0.42f, 0.18f, 0.08f, 1.0f))); + if (UVerticalBoxSlot* TitleSlot = RootBox->AddChildToVerticalBox(TitleText)) + { + TitleSlot->SetPadding(FMargin(2.0f, 0.0f, 0.0f, 12.0f)); + } + + GroupsScrollBox = WidgetTree->ConstructWidget(UScrollBox::StaticClass(), TEXT("GroupsScrollBox")); + if (UVerticalBoxSlot* ScrollSlot = RootBox->AddChildToVerticalBox(GroupsScrollBox)) + { + ScrollSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); + } + + GroupsBox = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("GroupsBox")); + if (UScrollBoxSlot* GroupsSlot = Cast(GroupsScrollBox->AddChild(GroupsBox))) + { + GroupsSlot->SetPadding(FMargin(0.0f)); + } + + UpdatePageWidgets(); +} + +void UPHYAttributePageWidget::UpdatePageWidgets() +{ + if (TitleText) + { + TitleText->SetText(PageTitle); + } +} diff --git a/Source/PHY/Private/UI/PHYAttributeResourceBarWidget.cpp b/Source/PHY/Private/UI/PHYAttributeResourceBarWidget.cpp new file mode 100644 index 0000000..3fc1590 --- /dev/null +++ b/Source/PHY/Private/UI/PHYAttributeResourceBarWidget.cpp @@ -0,0 +1,110 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "UI/PHYAttributeResourceBarWidget.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAttributeResourceBarWidget) + +#include "Blueprint/WidgetTree.h" +#include "Components/HorizontalBox.h" +#include "Components/HorizontalBoxSlot.h" +#include "Components/ProgressBar.h" +#include "Components/TextBlock.h" +#include "Components/VerticalBox.h" +#include "Components/VerticalBoxSlot.h" +#include "Fonts/SlateFontInfo.h" +#include "Styling/CoreStyle.h" +#include "Widgets/SWidget.h" + +namespace +{ + float MakePercent(const float CurrentValue, const float MaxValue) + { + return MaxValue > UE_SMALL_NUMBER ? FMath::Clamp(CurrentValue / MaxValue, 0.0f, 1.0f) : 0.0f; + } +} + +UPHYAttributeResourceBarWidget::UPHYAttributeResourceBarWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +TSharedRef UPHYAttributeResourceBarWidget::RebuildWidget() +{ + BuildNativeWidgetTree(); + return Super::RebuildWidget(); +} + +void UPHYAttributeResourceBarWidget::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + UpdateResourceWidgets(); +} + +void UPHYAttributeResourceBarWidget::SetResource(FText InLabel, const float InCurrentValue, const float InMaxValue, const FLinearColor InBarTint) +{ + Label = MoveTemp(InLabel); + CurrentValue = InCurrentValue; + MaxValue = FMath::Max(1.0f, InMaxValue); + BarTint = InBarTint; + UpdateResourceWidgets(); +} + +void UPHYAttributeResourceBarWidget::BuildNativeWidgetTree() +{ + if (!WidgetTree) + { + WidgetTree = NewObject(this, TEXT("WidgetTree"), RF_Transient); + } + + if (WidgetTree->RootWidget && LabelText && ValueText && ResourceBar) + { + return; + } + + UVerticalBox* RootBox = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("RootBox")); + WidgetTree->RootWidget = RootBox; + + UHorizontalBox* HeaderBox = WidgetTree->ConstructWidget(UHorizontalBox::StaticClass(), TEXT("HeaderBox")); + if (UVerticalBoxSlot* HeaderSlot = RootBox->AddChildToVerticalBox(HeaderBox)) + { + HeaderSlot->SetPadding(FMargin(0.0f, 2.0f, 0.0f, 2.0f)); + } + + LabelText = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), TEXT("LabelText")); + LabelText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 14)); + LabelText->SetColorAndOpacity(FSlateColor(FLinearColor(0.30f, 0.26f, 0.20f, 1.0f))); + if (UHorizontalBoxSlot* LabelSlot = HeaderBox->AddChildToHorizontalBox(LabelText)) + { + LabelSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); + } + + ValueText = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), TEXT("ValueText")); + ValueText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 14)); + ValueText->SetColorAndOpacity(FSlateColor(FLinearColor(0.10f, 0.26f, 0.23f, 1.0f))); + HeaderBox->AddChildToHorizontalBox(ValueText); + + ResourceBar = WidgetTree->ConstructWidget(UProgressBar::StaticClass(), TEXT("ResourceBar")); + if (UVerticalBoxSlot* BarSlot = RootBox->AddChildToVerticalBox(ResourceBar)) + { + BarSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 7.0f)); + } + + UpdateResourceWidgets(); +} + +void UPHYAttributeResourceBarWidget::UpdateResourceWidgets() +{ + if (LabelText) + { + LabelText->SetText(Label); + } + if (ValueText) + { + ValueText->SetText(FText::FromString(FString::Printf(TEXT("%.0f / %.0f"), FMath::Max(0.0f, CurrentValue), FMath::Max(1.0f, MaxValue)))); + } + if (ResourceBar) + { + ResourceBar->SetPercent(MakePercent(CurrentValue, MaxValue)); + ResourceBar->SetFillColorAndOpacity(BarTint); + } +} diff --git a/Source/PHY/Private/UI/PHYAttributeRowWidget.cpp b/Source/PHY/Private/UI/PHYAttributeRowWidget.cpp new file mode 100644 index 0000000..1118076 --- /dev/null +++ b/Source/PHY/Private/UI/PHYAttributeRowWidget.cpp @@ -0,0 +1,107 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "UI/PHYAttributeRowWidget.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAttributeRowWidget) + +#include "Blueprint/WidgetTree.h" +#include "Components/Button.h" +#include "Components/HorizontalBox.h" +#include "Components/HorizontalBoxSlot.h" +#include "Components/TextBlock.h" +#include "Fonts/SlateFontInfo.h" +#include "Styling/CoreStyle.h" +#include "Widgets/SWidget.h" + +UPHYAttributeRowWidget::UPHYAttributeRowWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +TSharedRef UPHYAttributeRowWidget::RebuildWidget() +{ + BuildNativeWidgetTree(); + return Super::RebuildWidget(); +} + +void UPHYAttributeRowWidget::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + UpdateRowWidgets(); +} + +void UPHYAttributeRowWidget::SetRow(FText InLabel, FText InValue, const bool bInCanUpgrade) +{ + Label = MoveTemp(InLabel); + Value = MoveTemp(InValue); + bCanUpgrade = bInCanUpgrade; + UpdateRowWidgets(); +} + +void UPHYAttributeRowWidget::BuildNativeWidgetTree() +{ + if (!WidgetTree) + { + WidgetTree = NewObject(this, TEXT("WidgetTree"), RF_Transient); + } + + if (WidgetTree->RootWidget && RowBox && LabelText && ValueText && UpgradeButton && UpgradeText) + { + return; + } + + RowBox = WidgetTree->ConstructWidget(UHorizontalBox::StaticClass(), TEXT("RowBox")); + WidgetTree->RootWidget = RowBox; + + LabelText = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), TEXT("LabelText")); + LabelText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 15)); + LabelText->SetColorAndOpacity(FSlateColor(FLinearColor(0.28f, 0.25f, 0.20f, 1.0f))); + if (UHorizontalBoxSlot* LabelSlot = RowBox->AddChildToHorizontalBox(LabelText)) + { + LabelSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); + LabelSlot->SetPadding(FMargin(0.0f, 4.0f, 10.0f, 4.0f)); + LabelSlot->SetVerticalAlignment(VAlign_Center); + } + + ValueText = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), TEXT("ValueText")); + ValueText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 15)); + ValueText->SetColorAndOpacity(FSlateColor(FLinearColor(0.08f, 0.24f, 0.22f, 1.0f))); + if (UHorizontalBoxSlot* ValueSlot = RowBox->AddChildToHorizontalBox(ValueText)) + { + ValueSlot->SetSize(FSlateChildSize(ESlateSizeRule::Automatic)); + ValueSlot->SetPadding(FMargin(0.0f, 4.0f, 8.0f, 4.0f)); + ValueSlot->SetVerticalAlignment(VAlign_Center); + } + + UpgradeButton = WidgetTree->ConstructWidget(UButton::StaticClass(), TEXT("UpgradeButton")); + UpgradeButton->SetBackgroundColor(FLinearColor(0.67f, 0.16f, 0.10f, 0.85f)); + UpgradeText = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), TEXT("UpgradeText")); + UpgradeText->SetText(FText::FromString(TEXT("+"))); + UpgradeText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 14)); + UpgradeText->SetColorAndOpacity(FSlateColor(FLinearColor(0.98f, 0.92f, 0.78f, 1.0f))); + UpgradeButton->SetContent(UpgradeText); + if (UHorizontalBoxSlot* UpgradeSlot = RowBox->AddChildToHorizontalBox(UpgradeButton)) + { + UpgradeSlot->SetSize(FSlateChildSize(ESlateSizeRule::Automatic)); + UpgradeSlot->SetPadding(FMargin(0.0f, 3.0f, 0.0f, 3.0f)); + UpgradeSlot->SetVerticalAlignment(VAlign_Center); + } + + UpdateRowWidgets(); +} + +void UPHYAttributeRowWidget::UpdateRowWidgets() +{ + if (LabelText) + { + LabelText->SetText(Label); + } + if (ValueText) + { + ValueText->SetText(Value); + } + if (UpgradeButton) + { + UpgradeButton->SetVisibility(bCanUpgrade ? ESlateVisibility::Visible : ESlateVisibility::Collapsed); + } +} diff --git a/Source/PHY/Private/UI/PHYAttributeSummaryWidget.cpp b/Source/PHY/Private/UI/PHYAttributeSummaryWidget.cpp new file mode 100644 index 0000000..89e47d1 --- /dev/null +++ b/Source/PHY/Private/UI/PHYAttributeSummaryWidget.cpp @@ -0,0 +1,153 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "UI/PHYAttributeSummaryWidget.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYAttributeSummaryWidget) + +#include "Blueprint/WidgetTree.h" +#include "Components/Border.h" +#include "Components/Spacer.h" +#include "Components/TextBlock.h" +#include "Components/VerticalBox.h" +#include "Components/VerticalBoxSlot.h" +#include "Fonts/SlateFontInfo.h" +#include "Styling/CoreStyle.h" +#include "UI/PHYAttributeResourceBarWidget.h" +#include "Widgets/SWidget.h" + +UPHYAttributeSummaryWidget::UPHYAttributeSummaryWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +TSharedRef UPHYAttributeSummaryWidget::RebuildWidget() +{ + BuildNativeWidgetTree(); + return Super::RebuildWidget(); +} + +void UPHYAttributeSummaryWidget::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + UpdateSummaryWidgets(); +} + +void UPHYAttributeSummaryWidget::SetSummary(FText InPlayerName, const int32 InLevel, const int32 InExperience, const int32 InExperienceForNextLevel, const int32 InCombatPower) +{ + PlayerName = MoveTemp(InPlayerName); + Level = FMath::Max(1, InLevel); + Experience = FMath::Max(0, InExperience); + ExperienceForNextLevel = FMath::Max(1, InExperienceForNextLevel); + CombatPower = FMath::Max(0, InCombatPower); + UpdateSummaryWidgets(); +} + +void UPHYAttributeSummaryWidget::SetResources(const float InHealth, const float InMaxHealth, const float InMana, const float InMaxMana, const float InStamina, const float InMaxStamina) +{ + Health = InHealth; + MaxHealth = FMath::Max(1.0f, InMaxHealth); + Mana = InMana; + MaxMana = FMath::Max(1.0f, InMaxMana); + Stamina = InStamina; + MaxStamina = FMath::Max(1.0f, InMaxStamina); + UpdateSummaryWidgets(); +} + +void UPHYAttributeSummaryWidget::BuildNativeWidgetTree() +{ + if (!WidgetTree) + { + WidgetTree = NewObject(this, TEXT("WidgetTree"), RF_Transient); + } + + if (WidgetTree->RootWidget && PlayerNameText && LevelText && CombatPowerText && ExperienceBar && HealthBar && ManaBar && StaminaBar) + { + return; + } + + UBorder* RootBorder = WidgetTree->ConstructWidget(UBorder::StaticClass(), TEXT("RootBorder")); + RootBorder->SetBrushColor(FLinearColor(0.92f, 0.86f, 0.70f, 0.55f)); + RootBorder->SetPadding(FMargin(18.0f)); + WidgetTree->RootWidget = RootBorder; + + UVerticalBox* RootBox = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("RootBox")); + RootBorder->SetContent(RootBox); + + PlayerNameText = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), TEXT("PlayerNameText")); + PlayerNameText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 24)); + PlayerNameText->SetColorAndOpacity(FSlateColor(FLinearColor(0.28f, 0.16f, 0.08f, 1.0f))); + RootBox->AddChildToVerticalBox(PlayerNameText); + + LevelText = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), TEXT("LevelText")); + LevelText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 17)); + LevelText->SetColorAndOpacity(FSlateColor(FLinearColor(0.14f, 0.34f, 0.30f, 1.0f))); + if (UVerticalBoxSlot* LevelSlot = RootBox->AddChildToVerticalBox(LevelText)) + { + LevelSlot->SetPadding(FMargin(0.0f, 8.0f, 0.0f, 0.0f)); + } + + CombatPowerText = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), TEXT("CombatPowerText")); + CombatPowerText->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 17)); + CombatPowerText->SetColorAndOpacity(FSlateColor(FLinearColor(0.60f, 0.34f, 0.08f, 1.0f))); + if (UVerticalBoxSlot* PowerSlot = RootBox->AddChildToVerticalBox(CombatPowerText)) + { + PowerSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 14.0f)); + } + + UTextBlock* PortraitPlaceholder = WidgetTree->ConstructWidget(UTextBlock::StaticClass(), TEXT("PortraitPlaceholder")); + PortraitPlaceholder->SetText(FText::FromString(TEXT("云纹角色剪影"))); + PortraitPlaceholder->SetFont(FSlateFontInfo(FCoreStyle::GetDefaultFont(), 20)); + PortraitPlaceholder->SetColorAndOpacity(FSlateColor(FLinearColor(0.38f, 0.45f, 0.38f, 0.85f))); + if (UVerticalBoxSlot* PortraitSlot = RootBox->AddChildToVerticalBox(PortraitPlaceholder)) + { + PortraitSlot->SetPadding(FMargin(0.0f, 22.0f, 0.0f, 24.0f)); + PortraitSlot->SetHorizontalAlignment(HAlign_Center); + } + + ResourceBox = WidgetTree->ConstructWidget(UVerticalBox::StaticClass(), TEXT("ResourceBox")); + RootBox->AddChildToVerticalBox(ResourceBox); + + ExperienceBar = WidgetTree->ConstructWidget(UPHYAttributeResourceBarWidget::StaticClass(), TEXT("ExperienceBar")); + HealthBar = WidgetTree->ConstructWidget(UPHYAttributeResourceBarWidget::StaticClass(), TEXT("HealthBar")); + ManaBar = WidgetTree->ConstructWidget(UPHYAttributeResourceBarWidget::StaticClass(), TEXT("ManaBar")); + StaminaBar = WidgetTree->ConstructWidget(UPHYAttributeResourceBarWidget::StaticClass(), TEXT("StaminaBar")); + + ResourceBox->AddChildToVerticalBox(ExperienceBar); + ResourceBox->AddChildToVerticalBox(HealthBar); + ResourceBox->AddChildToVerticalBox(ManaBar); + ResourceBox->AddChildToVerticalBox(StaminaBar); + + UpdateSummaryWidgets(); +} + +void UPHYAttributeSummaryWidget::UpdateSummaryWidgets() +{ + if (PlayerNameText) + { + PlayerNameText->SetText(PlayerName.IsEmpty() ? FText::FromString(TEXT("修行者")) : PlayerName); + } + if (LevelText) + { + LevelText->SetText(FText::FromString(FString::Printf(TEXT("等级 %d"), Level))); + } + if (CombatPowerText) + { + CombatPowerText->SetText(FText::FromString(FString::Printf(TEXT("战力 %d"), CombatPower))); + } + if (ExperienceBar) + { + ExperienceBar->SetResource(FText::FromString(TEXT("经验")), static_cast(Experience), static_cast(ExperienceForNextLevel), FLinearColor(0.80f, 0.55f, 0.18f, 1.0f)); + } + if (HealthBar) + { + HealthBar->SetResource(FText::FromString(TEXT("生命")), Health, MaxHealth, FLinearColor(0.72f, 0.16f, 0.12f, 1.0f)); + } + if (ManaBar) + { + ManaBar->SetResource(FText::FromString(TEXT("法力")), Mana, MaxMana, FLinearColor(0.22f, 0.38f, 0.86f, 1.0f)); + } + if (StaminaBar) + { + StaminaBar->SetResource(FText::FromString(TEXT("耐力")), Stamina, MaxStamina, FLinearColor(0.18f, 0.60f, 0.54f, 1.0f)); + } +} diff --git a/Source/PHY/Private/UI/PHYCombatDerivedAttributePageWidget.cpp b/Source/PHY/Private/UI/PHYCombatDerivedAttributePageWidget.cpp new file mode 100644 index 0000000..ba0a51a --- /dev/null +++ b/Source/PHY/Private/UI/PHYCombatDerivedAttributePageWidget.cpp @@ -0,0 +1,72 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "UI/PHYCombatDerivedAttributePageWidget.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYCombatDerivedAttributePageWidget) + +#include "AbilitySystemComponent.h" +#include "AbilitySystem/Attributes/PHYCombatAttributeSet.h" +#include "UI/PHYAttributeGroupWidget.h" + +namespace +{ + float GetAttributeValue(const UAbilitySystemComponent* AbilitySystemComponent, const FGameplayAttribute& Attribute) + { + return AbilitySystemComponent ? AbilitySystemComponent->GetNumericAttribute(Attribute) : 0.0f; + } + + FText MakeNumberText(const float Value) + { + return FText::FromString(FString::Printf(TEXT("%.0f"), Value)); + } + + FText MakePercentText(const float Value) + { + return FText::FromString(FString::Printf(TEXT("%.1f%%"), Value * 100.0f)); + } + + FText MakeMultiplierText(const float Value) + { + return FText::FromString(FString::Printf(TEXT("x%.2f"), Value)); + } + + void AddAttributeRow(UPHYAttributeGroupWidget* GroupWidget, const TCHAR* Label, const FText& Value) + { + if (GroupWidget) + { + GroupWidget->AddRow(FText::FromString(Label), Value); + } + } +} + +void UPHYCombatDerivedAttributePageWidget::RefreshAttributes(UAbilitySystemComponent* AbilitySystemComponent) +{ + SetPageTitle(FText::FromString(TEXT("战斗衍生属性"))); + ClearGroups(); + + UPHYAttributeGroupWidget* ResourceGroup = AddGroup(FText::FromString(TEXT("资源上限"))); + AddAttributeRow(ResourceGroup, TEXT("当前生命"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetHealthAttribute()))); + AddAttributeRow(ResourceGroup, TEXT("生命上限"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxHealthAttribute()))); + AddAttributeRow(ResourceGroup, TEXT("当前法力"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetManaAttribute()))); + AddAttributeRow(ResourceGroup, TEXT("法力上限"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxManaAttribute()))); + AddAttributeRow(ResourceGroup, TEXT("当前耐力"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetStaminaAttribute()))); + AddAttributeRow(ResourceGroup, TEXT("耐力上限"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetMaxStaminaAttribute()))); + + UPHYAttributeGroupWidget* AttackGroup = AddGroup(FText::FromString(TEXT("攻击能力"))); + AddAttributeRow(AttackGroup, TEXT("物理攻击"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetPhysicalAttackPowerAttribute()))); + AddAttributeRow(AttackGroup, TEXT("法术强度"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetSpellPowerAttribute()))); + AddAttributeRow(AttackGroup, TEXT("命中率"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetAccuracyAttribute()))); + AddAttributeRow(AttackGroup, TEXT("暴击率"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetCriticalChanceAttribute()))); + AddAttributeRow(AttackGroup, TEXT("暴击伤害"), MakeMultiplierText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetCriticalDamageAttribute()))); + AddAttributeRow(AttackGroup, TEXT("攻击速度"), MakeMultiplierText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetAttackSpeedAttribute()))); + AddAttributeRow(AttackGroup, TEXT("冷却缩减"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetCooldownReductionAttribute()))); + AddAttributeRow(AttackGroup, TEXT("破防强度"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetGuardBreakPowerAttribute()))); + AddAttributeRow(AttackGroup, TEXT("韧性伤害"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetPoiseDamageAttribute()))); + + UPHYAttributeGroupWidget* DefenseGroup = AddGroup(FText::FromString(TEXT("防御能力"))); + AddAttributeRow(DefenseGroup, TEXT("护甲"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetArmorAttribute()))); + AddAttributeRow(DefenseGroup, TEXT("魔法抗性"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetMagicResistanceAttribute()))); + AddAttributeRow(DefenseGroup, TEXT("闪避率"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetEvasionAttribute()))); + AddAttributeRow(DefenseGroup, TEXT("格挡强度"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetBlockPowerAttribute()))); + AddAttributeRow(DefenseGroup, TEXT("韧性"), MakeNumberText(GetAttributeValue(AbilitySystemComponent, UPHYCombatAttributeSet::GetPoiseAttribute()))); +} diff --git a/Source/PHY/Private/UI/PHYElementDerivedAttributePageWidget.cpp b/Source/PHY/Private/UI/PHYElementDerivedAttributePageWidget.cpp new file mode 100644 index 0000000..00813c6 --- /dev/null +++ b/Source/PHY/Private/UI/PHYElementDerivedAttributePageWidget.cpp @@ -0,0 +1,57 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "UI/PHYElementDerivedAttributePageWidget.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(PHYElementDerivedAttributePageWidget) + +#include "AbilitySystemComponent.h" +#include "AbilitySystem/Attributes/PHYElementAttributeSet.h" +#include "UI/PHYAttributeGroupWidget.h" + +namespace +{ + float GetAttributeValue(const UAbilitySystemComponent* AbilitySystemComponent, const FGameplayAttribute& Attribute) + { + return AbilitySystemComponent ? AbilitySystemComponent->GetNumericAttribute(Attribute) : 0.0f; + } + + FText MakePercentText(const float Value) + { + return FText::FromString(FString::Printf(TEXT("%.1f%%"), Value * 100.0f)); + } + + void AddAttributeRow(UPHYAttributeGroupWidget* GroupWidget, const TCHAR* Label, const FText& Value) + { + if (GroupWidget) + { + GroupWidget->AddRow(FText::FromString(Label), Value); + } + } +} + +void UPHYElementDerivedAttributePageWidget::RefreshAttributes(UAbilitySystemComponent* AbilitySystemComponent) +{ + SetPageTitle(FText::FromString(TEXT("元素衍生属性"))); + ClearGroups(); + + UPHYAttributeGroupWidget* BonusGroup = AddGroup(FText::FromString(TEXT("元素伤害加成"))); + AddAttributeRow(BonusGroup, TEXT("火伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetFireDamageBonusAttribute()))); + AddAttributeRow(BonusGroup, TEXT("水伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetWaterDamageBonusAttribute()))); + AddAttributeRow(BonusGroup, TEXT("冰伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetIceDamageBonusAttribute()))); + AddAttributeRow(BonusGroup, TEXT("雷伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetLightningDamageBonusAttribute()))); + AddAttributeRow(BonusGroup, TEXT("地伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetEarthDamageBonusAttribute()))); + AddAttributeRow(BonusGroup, TEXT("风伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetWindDamageBonusAttribute()))); + AddAttributeRow(BonusGroup, TEXT("光伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetLightDamageBonusAttribute()))); + AddAttributeRow(BonusGroup, TEXT("暗伤加成"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetDarkDamageBonusAttribute()))); + AddAttributeRow(BonusGroup, TEXT("元素穿透"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetElementPenetrationAttribute()))); + + UPHYAttributeGroupWidget* ResistanceGroup = AddGroup(FText::FromString(TEXT("元素抗性"))); + AddAttributeRow(ResistanceGroup, TEXT("火抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetFireResistanceAttribute()))); + AddAttributeRow(ResistanceGroup, TEXT("水抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetWaterResistanceAttribute()))); + AddAttributeRow(ResistanceGroup, TEXT("冰抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetIceResistanceAttribute()))); + AddAttributeRow(ResistanceGroup, TEXT("雷抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetLightningResistanceAttribute()))); + AddAttributeRow(ResistanceGroup, TEXT("地抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetEarthResistanceAttribute()))); + AddAttributeRow(ResistanceGroup, TEXT("风抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetWindResistanceAttribute()))); + AddAttributeRow(ResistanceGroup, TEXT("光抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetLightResistanceAttribute()))); + AddAttributeRow(ResistanceGroup, TEXT("暗抗"), MakePercentText(GetAttributeValue(AbilitySystemComponent, UPHYElementAttributeSet::GetDarkResistanceAttribute()))); +} diff --git a/Source/PHY/Public/Game/PHYHUD.h b/Source/PHY/Public/Game/PHYHUD.h index ddd2053..7afd2ba 100644 --- a/Source/PHY/Public/Game/PHYHUD.h +++ b/Source/PHY/Public/Game/PHYHUD.h @@ -36,6 +36,10 @@ public: UFUNCTION(BlueprintCallable, BlueprintPure, Category="PHY|UI") bool IsHUDInitialized() const { return bHUDInitialized; } + /** @brief 打开角色属性菜单,默认推入 Generic UI Menu 层。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI") + UCommonActivatableWidget* OpenAttributeMenu(APlayerController* OwningPlayerController); + protected: /** @brief 注册 GenericUISystem 本地玩家根布局和项目 UI 上下文。 */ virtual bool InitializeGenericUIForPlayer(APlayerController* OwningPlayerController); @@ -43,6 +47,9 @@ protected: /** @brief 推入默认 HUD 控件。 */ virtual UCommonActivatableWidget* PushDefaultHUDWidget(APlayerController* OwningPlayerController); + /** @brief 推入指定 CommonUI 控件到 Menu 层。 */ + virtual UCommonActivatableWidget* PushMenuWidget(APlayerController* OwningPlayerController, TSubclassOf WidgetClass); + /** @brief 当前本地玩家 UI 上下文。 */ UPROPERTY(Transient) TObjectPtr PlayerUIContext; diff --git a/Source/PHY/Public/PHYConfigSettings.h b/Source/PHY/Public/PHYConfigSettings.h index 55276a4..cc3a4d8 100644 --- a/Source/PHY/Public/PHYConfigSettings.h +++ b/Source/PHY/Public/PHYConfigSettings.h @@ -167,6 +167,10 @@ public: UPROPERTY(Config) TSoftClassPtr DefaultHUDWidgetClass; + /** @brief 默认角色属性菜单控件类,用于推入 Menu 层。 */ + UPROPERTY(Config) + TSoftClassPtr AttributeMenuWidgetClass; + /** @brief 默认 gameplay 层 Tag。 */ UPROPERTY(Config) FGameplayTag GameplayLayerTag; diff --git a/Source/PHY/Public/UI/PHYAttributeGroupWidget.h b/Source/PHY/Public/UI/PHYAttributeGroupWidget.h new file mode 100644 index 0000000..4bacbad --- /dev/null +++ b/Source/PHY/Public/UI/PHYAttributeGroupWidget.h @@ -0,0 +1,63 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "PHYAttributeGroupWidget.generated.h" + +class UPHYAttributeRowWidget; +class UTextBlock; +class UVerticalBox; + +/** + * @brief 属性分组子控件。 + * + * 每个分组负责一个标题和若干属性行,例如攻击能力、防御能力、元素抗性。 + */ +UCLASS(BlueprintType, Blueprintable) +class PHY_API UPHYAttributeGroupWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + /** @brief 构造属性分组控件。 */ + UPHYAttributeGroupWidget(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** @brief 构建原生分组 WidgetTree。 */ + virtual TSharedRef RebuildWidget() override; + + /** @brief 同步分组显示。 */ + virtual void SynchronizeProperties() override; + + /** @brief 设置分组标题。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + void SetGroupTitle(FText InGroupTitle); + + /** @brief 清理当前分组内所有属性行。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + void ClearRows(); + + /** @brief 添加一行属性。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + UPHYAttributeRowWidget* AddRow(FText Label, FText Value, bool bCanUpgrade = false); + +protected: + /** @brief 构造原生分组树。 */ + void BuildNativeWidgetTree(); + + /** @brief 刷新分组标题。 */ + void UpdateGroupWidgets(); + + /** @brief 分组标题。 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes") + FText GroupTitle; + + /** @brief 标题文本。 */ + UPROPERTY(Transient) + TObjectPtr TitleText; + + /** @brief 属性行容器。 */ + UPROPERTY(Transient) + TObjectPtr RowsBox; +}; diff --git a/Source/PHY/Public/UI/PHYAttributeMenuWidget.h b/Source/PHY/Public/UI/PHYAttributeMenuWidget.h new file mode 100644 index 0000000..080a5e9 --- /dev/null +++ b/Source/PHY/Public/UI/PHYAttributeMenuWidget.h @@ -0,0 +1,164 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AttributeSet.h" +#include "GameplayEffectTypes.h" +#include "UI/GUIS_ActivatableWidget.h" +#include "PHYAttributeMenuWidget.generated.h" + +class APlayerController; +class APHYPlayerState; +class UAbilitySystemComponent; +class UButton; +class UHorizontalBox; +class UPHYAttributeOverviewPageWidget; +class UPHYCombatDerivedAttributePageWidget; +class UPHYElementDerivedAttributePageWidget; +class UPHYAttributeSummaryWidget; +class UTextBlock; +class UWidgetSwitcher; +struct FOnAttributeChangeData; + +/** + * @brief PHY 角色属性主界面。 + * + * 主界面只负责绑定 PlayerState/ASC、分页和数据分发;具体显示拆到概览、分页、分组、属性行和资源条子控件。 + */ +UCLASS(BlueprintType, Blueprintable) +class PHY_API UPHYAttributeMenuWidget : public UGUIS_ActivatableWidget +{ + GENERATED_BODY() + +public: + /** @brief 构造角色属性主界面。 */ + UPHYAttributeMenuWidget(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** @brief 构建原生属性界面 WidgetTree。 */ + virtual TSharedRef RebuildWidget() override; + + /** @brief 同步属性界面显示。 */ + virtual void SynchronizeProperties() override; + + /** @brief 绑定本地玩家控制器,随后从 PlayerState 和 GAS 拉取属性。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + void InitializeFromPlayer(APlayerController* InPlayerController); + + /** @brief 重新从当前绑定来源刷新所有分页。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + void RefreshFromBoundSources(); + +protected: + /** @brief Widget 构造后尝试自动绑定 OwningPlayer。 */ + virtual void NativeConstruct() override; + + /** @brief Widget 销毁前解除所有委托。 */ + virtual void NativeDestruct() override; + + /** @brief PlayerState 延迟到达时重试绑定。 */ + virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override; + + /** @brief 构造原生属性界面树。 */ + void BuildNativeWidgetTree(); + + /** @brief 创建一个分页按钮。 */ + UButton* CreateTabButton(FName ButtonName, FText Label); + + /** @brief 切换到指定分页。 */ + void SetActivePage(int32 NewPageIndex); + + /** @brief 刷新分页按钮颜色。 */ + void UpdateTabVisuals(); + + /** @brief 绑定新的 PlayerState。 */ + void BindToPlayerState(APHYPlayerState* NewPlayerState); + + /** @brief 解除 PlayerState 委托。 */ + void UnbindFromPlayerState(); + + /** @brief 绑定新的 AbilitySystemComponent。 */ + void BindToAbilitySystem(UAbilitySystemComponent* NewAbilitySystemComponent); + + /** @brief 解除 GAS 属性变化委托。 */ + void UnbindFromAbilitySystem(); + + /** @brief 重建概览和所有属性分页内容。 */ + void RefreshAttributePages(); + + /** @brief 属性变化后刷新分页。 */ + void HandleAttributeChanged(const FOnAttributeChangeData& ChangeData); + + /** @brief PlayerState 成长信息变化后刷新分页。 */ + UFUNCTION() + void HandleProgressionChanged(int32 NewLevel, int32 NewExperience, int32 NewExperienceForNextLevel); + + /** @brief 点击总览分页。 */ + UFUNCTION() + void HandleOverviewTabClicked(); + + /** @brief 点击战斗分页。 */ + UFUNCTION() + void HandleCombatTabClicked(); + + /** @brief 点击元素分页。 */ + UFUNCTION() + void HandleElementTabClicked(); + + /** @brief 读取一个 GAS 属性数值。 */ + float GetAttributeValue(const FGameplayAttribute& Attribute) const; + + /** @brief 计算仅用于 UI 概览的战力评分。 */ + int32 CalculateDisplayCombatPower() const; + + /** @brief 当前绑定的本地玩家控制器弱引用。 */ + TWeakObjectPtr BoundPlayerController; + + /** @brief 当前绑定的玩家状态弱引用。 */ + TWeakObjectPtr BoundPlayerState; + + /** @brief 当前绑定的 ASC 弱引用。 */ + TWeakObjectPtr BoundAbilitySystemComponent; + + /** @brief GAS 属性变化委托句柄。 */ + TArray AttributeChangedHandles; + + /** @brief GAS 属性变化委托对应属性。 */ + TArray BoundAttributes; + + /** @brief 当前分页索引。 */ + UPROPERTY(Transient, BlueprintReadOnly, Category="PHY|UI|Attributes") + int32 ActivePageIndex = 0; + + /** @brief 左侧概览子控件。 */ + UPROPERTY(Transient) + TObjectPtr SummaryWidget; + + /** @brief 分页切换器。 */ + UPROPERTY(Transient) + TObjectPtr PageSwitcher; + + /** @brief 总览分页。 */ + UPROPERTY(Transient) + TObjectPtr OverviewPage; + + /** @brief 战斗衍生属性分页。 */ + UPROPERTY(Transient) + TObjectPtr CombatPage; + + /** @brief 元素衍生属性分页。 */ + UPROPERTY(Transient) + TObjectPtr ElementPage; + + /** @brief 总览按钮。 */ + UPROPERTY(Transient) + TObjectPtr OverviewTabButton; + + /** @brief 战斗按钮。 */ + UPROPERTY(Transient) + TObjectPtr CombatTabButton; + + /** @brief 元素按钮。 */ + UPROPERTY(Transient) + TObjectPtr ElementTabButton; +}; diff --git a/Source/PHY/Public/UI/PHYAttributeOverviewPageWidget.h b/Source/PHY/Public/UI/PHYAttributeOverviewPageWidget.h new file mode 100644 index 0000000..4a240a8 --- /dev/null +++ b/Source/PHY/Public/UI/PHYAttributeOverviewPageWidget.h @@ -0,0 +1,25 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UI/PHYAttributePageWidget.h" +#include "PHYAttributeOverviewPageWidget.generated.h" + +class UAbilitySystemComponent; + +/** + * @brief 属性总览分页子控件。 + * + * 展示核心属性和三项主要战斗资源,核心属性保留加点按钮占位。 + */ +UCLASS(BlueprintType, Blueprintable) +class PHY_API UPHYAttributeOverviewPageWidget : public UPHYAttributePageWidget +{ + GENERATED_BODY() + +public: + /** @brief 从 ASC 刷新总览分页。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + void RefreshAttributes(UAbilitySystemComponent* AbilitySystemComponent); +}; diff --git a/Source/PHY/Public/UI/PHYAttributePageWidget.h b/Source/PHY/Public/UI/PHYAttributePageWidget.h new file mode 100644 index 0000000..0da457c --- /dev/null +++ b/Source/PHY/Public/UI/PHYAttributePageWidget.h @@ -0,0 +1,68 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "PHYAttributePageWidget.generated.h" + +class UPHYAttributeGroupWidget; +class UScrollBox; +class UTextBlock; +class UVerticalBox; + +/** + * @brief 属性分页子控件。 + * + * 分页只负责标题和分组容器,具体属性项由分组和属性行子控件承载。 + */ +UCLASS(BlueprintType, Blueprintable) +class PHY_API UPHYAttributePageWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + /** @brief 构造属性分页控件。 */ + UPHYAttributePageWidget(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** @brief 构建原生分页 WidgetTree。 */ + virtual TSharedRef RebuildWidget() override; + + /** @brief 同步分页显示。 */ + virtual void SynchronizeProperties() override; + + /** @brief 设置分页标题。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + void SetPageTitle(FText InPageTitle); + + /** @brief 清理当前分页内所有分组。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + void ClearGroups(); + + /** @brief 添加一个属性分组。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + UPHYAttributeGroupWidget* AddGroup(FText GroupTitle); + +protected: + /** @brief 构造原生分页树。 */ + void BuildNativeWidgetTree(); + + /** @brief 刷新分页标题。 */ + void UpdatePageWidgets(); + + /** @brief 分页标题。 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes") + FText PageTitle; + + /** @brief 标题文本。 */ + UPROPERTY(Transient) + TObjectPtr TitleText; + + /** @brief 分组滚动区域。 */ + UPROPERTY(Transient) + TObjectPtr GroupsScrollBox; + + /** @brief 分组容器。 */ + UPROPERTY(Transient) + TObjectPtr GroupsBox; +}; diff --git a/Source/PHY/Public/UI/PHYAttributeResourceBarWidget.h b/Source/PHY/Public/UI/PHYAttributeResourceBarWidget.h new file mode 100644 index 0000000..948d61c --- /dev/null +++ b/Source/PHY/Public/UI/PHYAttributeResourceBarWidget.h @@ -0,0 +1,70 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "PHYAttributeResourceBarWidget.generated.h" + +class UProgressBar; +class UTextBlock; + +/** + * @brief 资源条子控件。 + * + * 用于生命、法力、耐力和经验等当前值/上限值显示。 + */ +UCLASS(BlueprintType, Blueprintable) +class PHY_API UPHYAttributeResourceBarWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + /** @brief 构造资源条控件。 */ + UPHYAttributeResourceBarWidget(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** @brief 构建原生资源条 WidgetTree。 */ + virtual TSharedRef RebuildWidget() override; + + /** @brief 同步资源条显示。 */ + virtual void SynchronizeProperties() override; + + /** @brief 设置资源条内容。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + void SetResource(FText InLabel, float InCurrentValue, float InMaxValue, FLinearColor InBarTint); + +protected: + /** @brief 构造原生资源条树。 */ + void BuildNativeWidgetTree(); + + /** @brief 刷新资源条显示。 */ + void UpdateResourceWidgets(); + + /** @brief 资源名称。 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes") + FText Label; + + /** @brief 当前值。 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes") + float CurrentValue = 0.0f; + + /** @brief 上限值。 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes") + float MaxValue = 1.0f; + + /** @brief 填充颜色。 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes") + FLinearColor BarTint = FLinearColor(0.25f, 0.68f, 0.63f, 1.0f); + + /** @brief 名称文本。 */ + UPROPERTY(Transient) + TObjectPtr LabelText; + + /** @brief 数值文本。 */ + UPROPERTY(Transient) + TObjectPtr ValueText; + + /** @brief 进度条。 */ + UPROPERTY(Transient) + TObjectPtr ResourceBar; +}; diff --git a/Source/PHY/Public/UI/PHYAttributeRowWidget.h b/Source/PHY/Public/UI/PHYAttributeRowWidget.h new file mode 100644 index 0000000..75fa885 --- /dev/null +++ b/Source/PHY/Public/UI/PHYAttributeRowWidget.h @@ -0,0 +1,75 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "PHYAttributeRowWidget.generated.h" + +class UButton; +class UHorizontalBox; +class UTextBlock; + +/** + * @brief 属性行子控件。 + * + * 用于显示单个属性名称、当前数值和可选加点按钮,供属性页分组复用。 + */ +UCLASS(BlueprintType, Blueprintable) +class PHY_API UPHYAttributeRowWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + /** @brief 构造属性行控件。 */ + UPHYAttributeRowWidget(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** @brief 构建原生属性行 WidgetTree。 */ + virtual TSharedRef RebuildWidget() override; + + /** @brief 同步属性行显示。 */ + virtual void SynchronizeProperties() override; + + /** @brief 设置属性行内容。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + void SetRow(FText InLabel, FText InValue, bool bInCanUpgrade = false); + +protected: + /** @brief 构造原生属性行树。 */ + void BuildNativeWidgetTree(); + + /** @brief 刷新属性行显示。 */ + void UpdateRowWidgets(); + + /** @brief 属性名称。 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes") + FText Label; + + /** @brief 属性数值文本。 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes") + FText Value; + + /** @brief 是否显示加点按钮。 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes") + bool bCanUpgrade = false; + + /** @brief 行根容器。 */ + UPROPERTY(Transient) + TObjectPtr RowBox; + + /** @brief 属性名称文本。 */ + UPROPERTY(Transient) + TObjectPtr LabelText; + + /** @brief 属性数值文本。 */ + UPROPERTY(Transient) + TObjectPtr ValueText; + + /** @brief 加点按钮。 */ + UPROPERTY(Transient) + TObjectPtr UpgradeButton; + + /** @brief 加点按钮文本。 */ + UPROPERTY(Transient) + TObjectPtr UpgradeText; +}; diff --git a/Source/PHY/Public/UI/PHYAttributeSummaryWidget.h b/Source/PHY/Public/UI/PHYAttributeSummaryWidget.h new file mode 100644 index 0000000..b1a731a --- /dev/null +++ b/Source/PHY/Public/UI/PHYAttributeSummaryWidget.h @@ -0,0 +1,106 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "PHYAttributeSummaryWidget.generated.h" + +class UPHYAttributeResourceBarWidget; +class UTextBlock; +class UVerticalBox; + +/** + * @brief 属性界面左侧概览子控件。 + * + * 展示角色名称、等级、战力、经验条和三项主要战斗资源。 + */ +UCLASS(BlueprintType, Blueprintable) +class PHY_API UPHYAttributeSummaryWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + /** @brief 构造属性概览控件。 */ + UPHYAttributeSummaryWidget(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** @brief 构建原生概览 WidgetTree。 */ + virtual TSharedRef RebuildWidget() override; + + /** @brief 同步概览显示。 */ + virtual void SynchronizeProperties() override; + + /** @brief 设置角色概览信息。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + void SetSummary(FText InPlayerName, int32 InLevel, int32 InExperience, int32 InExperienceForNextLevel, int32 InCombatPower); + + /** @brief 设置三项主要资源。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + void SetResources(float InHealth, float InMaxHealth, float InMana, float InMaxMana, float InStamina, float InMaxStamina); + +protected: + /** @brief 构造原生概览树。 */ + void BuildNativeWidgetTree(); + + /** @brief 刷新概览显示。 */ + void UpdateSummaryWidgets(); + + /** @brief 角色名称。 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes") + FText PlayerName; + + /** @brief 当前等级。 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes") + int32 Level = 1; + + /** @brief 当前经验。 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes") + int32 Experience = 0; + + /** @brief 下一级所需经验。 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes") + int32 ExperienceForNextLevel = 100; + + /** @brief 概览战力评分。 */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="PHY|UI|Attributes") + int32 CombatPower = 0; + + float Health = 0.0f; + float MaxHealth = 1.0f; + float Mana = 0.0f; + float MaxMana = 1.0f; + float Stamina = 0.0f; + float MaxStamina = 1.0f; + + /** @brief 角色名文本。 */ + UPROPERTY(Transient) + TObjectPtr PlayerNameText; + + /** @brief 等级文本。 */ + UPROPERTY(Transient) + TObjectPtr LevelText; + + /** @brief 战力文本。 */ + UPROPERTY(Transient) + TObjectPtr CombatPowerText; + + /** @brief 资源条容器。 */ + UPROPERTY(Transient) + TObjectPtr ResourceBox; + + /** @brief 经验条。 */ + UPROPERTY(Transient) + TObjectPtr ExperienceBar; + + /** @brief 生命条。 */ + UPROPERTY(Transient) + TObjectPtr HealthBar; + + /** @brief 法力条。 */ + UPROPERTY(Transient) + TObjectPtr ManaBar; + + /** @brief 耐力条。 */ + UPROPERTY(Transient) + TObjectPtr StaminaBar; +}; diff --git a/Source/PHY/Public/UI/PHYCombatDerivedAttributePageWidget.h b/Source/PHY/Public/UI/PHYCombatDerivedAttributePageWidget.h new file mode 100644 index 0000000..90c5e5a --- /dev/null +++ b/Source/PHY/Public/UI/PHYCombatDerivedAttributePageWidget.h @@ -0,0 +1,25 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UI/PHYAttributePageWidget.h" +#include "PHYCombatDerivedAttributePageWidget.generated.h" + +class UAbilitySystemComponent; + +/** + * @brief 战斗衍生属性分页子控件。 + * + * 展示战斗资源、攻击能力和防御能力内的全部战斗衍生属性。 + */ +UCLASS(BlueprintType, Blueprintable) +class PHY_API UPHYCombatDerivedAttributePageWidget : public UPHYAttributePageWidget +{ + GENERATED_BODY() + +public: + /** @brief 从 ASC 刷新战斗衍生属性分页。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + void RefreshAttributes(UAbilitySystemComponent* AbilitySystemComponent); +}; diff --git a/Source/PHY/Public/UI/PHYElementDerivedAttributePageWidget.h b/Source/PHY/Public/UI/PHYElementDerivedAttributePageWidget.h new file mode 100644 index 0000000..2341d79 --- /dev/null +++ b/Source/PHY/Public/UI/PHYElementDerivedAttributePageWidget.h @@ -0,0 +1,25 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UI/PHYAttributePageWidget.h" +#include "PHYElementDerivedAttributePageWidget.generated.h" + +class UAbilitySystemComponent; + +/** + * @brief 元素衍生属性分页子控件。 + * + * 展示全部元素伤害加成、元素抗性和通用元素穿透。 + */ +UCLASS(BlueprintType, Blueprintable) +class PHY_API UPHYElementDerivedAttributePageWidget : public UPHYAttributePageWidget +{ + GENERATED_BODY() + +public: + /** @brief 从 ASC 刷新元素衍生属性分页。 */ + UFUNCTION(BlueprintCallable, Category="PHY|UI|Attributes") + void RefreshAttributes(UAbilitySystemComponent* AbilitySystemComponent); +};