297 lines
8.2 KiB
C++
297 lines
8.2 KiB
C++
#include "SkillTreeConnectionsWidget.h"
|
|
#include "SkillTreeComponent.h"
|
|
#include "Blueprint/WidgetTree.h"
|
|
#include "Components/PanelWidget.h"
|
|
#include "Rendering/DrawElements.h"
|
|
#include "Blueprint/WidgetLayoutLibrary.h"
|
|
|
|
USkillTreeConnectionsWidget::USkillTreeConnectionsWidget(
|
|
const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
bNeedsRedraw = true;
|
|
}
|
|
|
|
void USkillTreeConnectionsWidget::NativeConstruct()
|
|
{
|
|
Super::NativeConstruct();
|
|
|
|
// Bind to skill tree component events for automatic refresh
|
|
if (SkillTreeComponent)
|
|
{
|
|
// Remove any existing bindings first to prevent double-binding
|
|
SkillTreeComponent->OnSkillStateChanged.RemoveDynamic(
|
|
this, &USkillTreeConnectionsWidget::OnSkillStateChangedHandler);
|
|
SkillTreeComponent->OnSkillSelectionChanged.RemoveDynamic(
|
|
this, &USkillTreeConnectionsWidget::OnSkillSelectionChangedHandler);
|
|
SkillTreeComponent->OnSkillPurchased.RemoveDynamic(
|
|
this, &USkillTreeConnectionsWidget::OnSkillPurchasedHandler);
|
|
|
|
// Now bind
|
|
SkillTreeComponent->OnSkillStateChanged.AddDynamic(
|
|
this, &USkillTreeConnectionsWidget::OnSkillStateChangedHandler);
|
|
SkillTreeComponent->OnSkillSelectionChanged.AddDynamic(
|
|
this, &USkillTreeConnectionsWidget::OnSkillSelectionChangedHandler);
|
|
SkillTreeComponent->OnSkillPurchased.AddDynamic(
|
|
this, &USkillTreeConnectionsWidget::OnSkillPurchasedHandler);
|
|
}
|
|
}
|
|
|
|
void USkillTreeConnectionsWidget::NativeDestruct()
|
|
{
|
|
// Unbind from events to prevent memory leaks
|
|
if (SkillTreeComponent)
|
|
{
|
|
SkillTreeComponent->OnSkillStateChanged.RemoveDynamic(
|
|
this, &USkillTreeConnectionsWidget::OnSkillStateChangedHandler);
|
|
SkillTreeComponent->OnSkillSelectionChanged.RemoveDynamic(
|
|
this, &USkillTreeConnectionsWidget::OnSkillSelectionChangedHandler);
|
|
SkillTreeComponent->OnSkillPurchased.RemoveDynamic(
|
|
this, &USkillTreeConnectionsWidget::OnSkillPurchasedHandler);
|
|
}
|
|
|
|
Super::NativeDestruct();
|
|
}
|
|
|
|
void USkillTreeConnectionsWidget::RefreshConnections()
|
|
{
|
|
bNeedsRedraw = true;
|
|
// Force widget to repaint
|
|
if (IsValid(this))
|
|
{
|
|
Invalidate(EInvalidateWidgetReason::Paint);
|
|
}
|
|
}
|
|
|
|
void USkillTreeConnectionsWidget::OnSkillStateChangedHandler(FName SkillID)
|
|
{
|
|
RefreshConnections();
|
|
}
|
|
|
|
void USkillTreeConnectionsWidget::OnSkillSelectionChangedHandler(
|
|
int32 TotalCost)
|
|
{
|
|
RefreshConnections();
|
|
}
|
|
|
|
void USkillTreeConnectionsWidget::OnSkillPurchasedHandler(FName SkillID)
|
|
{
|
|
RefreshConnections();
|
|
}
|
|
|
|
bool USkillTreeConnectionsWidget::GetSkillIDFromWidget(UUserWidget* Widget,
|
|
FName& OutSkillID) const
|
|
{
|
|
if (!Widget)
|
|
return false;
|
|
|
|
// Try to find a property named "SkillID" on the widget
|
|
for (TFieldIterator<FProperty> PropIt(Widget->GetClass()); PropIt; ++PropIt)
|
|
{
|
|
FProperty* Property = *PropIt;
|
|
if (Property && Property->GetFName() == FName(TEXT("SkillID")))
|
|
{
|
|
// Check if it's a FName property
|
|
if (FNameProperty* NameProperty =
|
|
CastField<FNameProperty>(Property))
|
|
{
|
|
const void* ValuePtr =
|
|
Property->ContainerPtrToValuePtr<void>(Widget);
|
|
OutSkillID = NameProperty->GetPropertyValue(ValuePtr);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void USkillTreeConnectionsWidget::FindWidgetsOfClass(
|
|
UWidget* Root, TSubclassOf<UUserWidget> WidgetClass,
|
|
TArray<UUserWidget*>& OutWidgets) const
|
|
{
|
|
if (!Root || !WidgetClass)
|
|
return;
|
|
|
|
// Check if this widget is a UserWidget of the desired class
|
|
if (UUserWidget* UserWidget = Cast<UUserWidget>(Root))
|
|
{
|
|
if (UserWidget->IsA(WidgetClass))
|
|
{
|
|
OutWidgets.Add(UserWidget);
|
|
}
|
|
}
|
|
|
|
// If this is a panel widget, recursively search its children
|
|
if (UPanelWidget* PanelWidget = Cast<UPanelWidget>(Root))
|
|
{
|
|
for (int32 i = 0; i < PanelWidget->GetChildrenCount(); ++i)
|
|
{
|
|
UWidget* Child = PanelWidget->GetChildAt(i);
|
|
FindWidgetsOfClass(Child, WidgetClass, OutWidgets);
|
|
}
|
|
}
|
|
|
|
// If this is a UserWidget, search its widget tree
|
|
if (UUserWidget* UserWidget = Cast<UUserWidget>(Root))
|
|
{
|
|
if (UserWidget->WidgetTree)
|
|
{
|
|
TArray<UWidget*> AllWidgets;
|
|
UserWidget->WidgetTree->GetAllWidgets(AllWidgets);
|
|
|
|
for (UWidget* Widget : AllWidgets)
|
|
{
|
|
if (Widget != Root) // Avoid infinite recursion
|
|
{
|
|
FindWidgetsOfClass(Widget, WidgetClass, OutWidgets);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void USkillTreeConnectionsWidget::BuildNodeInfoList(
|
|
TArray<FSkillNodeInfo>& OutNodeInfo) const
|
|
{
|
|
OutNodeInfo.Empty();
|
|
|
|
if (!SkillTreeComponent || !SkillNodeWidgetClass)
|
|
return;
|
|
|
|
// Determine root widget to search from
|
|
UWidget* RootWidget = SkillTreeWidget;
|
|
if (!RootWidget)
|
|
{
|
|
// If no parent widget is set, try to get the outer widget
|
|
RootWidget = Cast<UWidget>(GetOuter());
|
|
if (!RootWidget)
|
|
{
|
|
// As last resort, try to get root widget from widget tree
|
|
if (WidgetTree)
|
|
{
|
|
RootWidget = WidgetTree->RootWidget;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!RootWidget)
|
|
{
|
|
UE_LOG(
|
|
LogTemp, Error,
|
|
TEXT(
|
|
"SkillTreeConnectionsWidget: No valid root widget found to search for skill nodes"));
|
|
return;
|
|
}
|
|
|
|
// Find all skill node widgets recursively
|
|
TArray<UUserWidget*> SkillNodeWidgets;
|
|
FindWidgetsOfClass(RootWidget, SkillNodeWidgetClass, SkillNodeWidgets);
|
|
|
|
// Build info for each skill node
|
|
for (UUserWidget* SkillNodeWidget : SkillNodeWidgets)
|
|
{
|
|
if (!SkillNodeWidget)
|
|
continue;
|
|
|
|
// Try to get the SkillID from the widget
|
|
FName SkillID;
|
|
if (!GetSkillIDFromWidget(SkillNodeWidget, SkillID))
|
|
continue;
|
|
|
|
// Get the skill node data from the component
|
|
FSkillNodeRuntime NodeData;
|
|
if (!SkillTreeComponent->GetSkillNodeData(SkillID, NodeData))
|
|
continue;
|
|
|
|
// Get the widget's geometry to determine its position
|
|
const FGeometry& Geometry = SkillNodeWidget->GetCachedGeometry();
|
|
FVector2D LocalSize = Geometry.GetLocalSize();
|
|
FVector2D AbsolutePosition = Geometry.GetAbsolutePosition();
|
|
|
|
// Calculate center position (in absolute/screen space)
|
|
FVector2D CenterPosition = AbsolutePosition + (LocalSize * 0.5f);
|
|
|
|
// Build the info struct
|
|
FSkillNodeInfo NodeInfo;
|
|
NodeInfo.SkillID = SkillID;
|
|
NodeInfo.Position = CenterPosition;
|
|
NodeInfo.State = NodeData.CurrentState;
|
|
NodeInfo.Prerequisites = NodeData.NodeData.Prerequisites;
|
|
|
|
OutNodeInfo.Add(NodeInfo);
|
|
}
|
|
}
|
|
|
|
FLinearColor
|
|
USkillTreeConnectionsWidget::GetLineColorForState(ESkillNodeState State) const
|
|
{
|
|
switch (State)
|
|
{
|
|
case ESkillNodeState::Locked:
|
|
return LockedColor;
|
|
case ESkillNodeState::Available:
|
|
return AvailableColor;
|
|
case ESkillNodeState::Selected:
|
|
return SelectedColor;
|
|
case ESkillNodeState::Purchased:
|
|
return PurchasedColor;
|
|
default:
|
|
return LockedColor;
|
|
}
|
|
}
|
|
|
|
int32 USkillTreeConnectionsWidget::NativePaint(
|
|
const FPaintArgs& Args, const FGeometry& AllottedGeometry,
|
|
const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements,
|
|
int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
|
|
{
|
|
int32 MaxLayerId = Super::NativePaint(Args, AllottedGeometry, MyCullingRect,
|
|
OutDrawElements, LayerId,
|
|
InWidgetStyle, bParentEnabled);
|
|
|
|
if (!SkillTreeComponent || !SkillNodeWidgetClass)
|
|
return MaxLayerId;
|
|
|
|
// Build node information
|
|
TArray<FSkillNodeInfo> NodeInfoList;
|
|
BuildNodeInfoList(NodeInfoList);
|
|
|
|
// Draw lines for each node's prerequisites
|
|
for (const FSkillNodeInfo& DependentNode : NodeInfoList)
|
|
{
|
|
// Get the color for this node's connections
|
|
FLinearColor LineColor = GetLineColorForState(DependentNode.State);
|
|
|
|
// Draw lines to each prerequisite
|
|
for (const FName& PrerequisiteID : DependentNode.Prerequisites)
|
|
{
|
|
// Find the prerequisite node in our list
|
|
const FSkillNodeInfo* PrereqNode = NodeInfoList.FindByPredicate(
|
|
[&PrerequisiteID](const FSkillNodeInfo& Info)
|
|
{ return Info.SkillID == PrerequisiteID; });
|
|
|
|
if (!PrereqNode)
|
|
continue;
|
|
|
|
// Convert absolute/screen positions to local space of this widget using Slate geometry
|
|
FVector2D PrereqLocalPos = AllottedGeometry.AbsoluteToLocal(PrereqNode->Position);
|
|
FVector2D DependentLocalPos = AllottedGeometry.AbsoluteToLocal(DependentNode.Position);
|
|
|
|
// Draw line from prerequisite to dependent
|
|
TArray<FVector2D> LinePoints;
|
|
LinePoints.Add(PrereqLocalPos);
|
|
LinePoints.Add(DependentLocalPos);
|
|
|
|
FSlateDrawElement::MakeLines(OutDrawElements, MaxLayerId + 1,
|
|
AllottedGeometry.ToPaintGeometry(),
|
|
LinePoints, ESlateDrawEffect::None,
|
|
LineColor,
|
|
true, // bAntialias
|
|
LineThickness);
|
|
}
|
|
}
|
|
|
|
return MaxLayerId + 1;
|
|
}
|