fix: selection

This commit is contained in:
2025-10-12 15:35:17 +02:00
parent 83e28d3828
commit bf15db0066
2 changed files with 475 additions and 464 deletions

View File

@@ -3,555 +3,569 @@
USkillTreeComponent::USkillTreeComponent() USkillTreeComponent::USkillTreeComponent()
{ {
PrimaryComponentTick.bCanEverTick = false; PrimaryComponentTick.bCanEverTick = false;
AvailableSkillPoints = 5; // Start with 5 skill points for testing AvailableSkillPoints = 5;
TotalSkillPointsEarned = 5; TotalSkillPointsEarned = 5;
CurrentSelectionCost = 0; CurrentSelectionCost = 0;
} }
void USkillTreeComponent::BeginPlay() void USkillTreeComponent::BeginPlay()
{ {
Super::BeginPlay(); Super::BeginPlay();
InitializeSkillTree(); InitializeSkillTree();
} }
void USkillTreeComponent::InitializeSkillTree() void USkillTreeComponent::InitializeSkillTree()
{ {
AllSkills.Empty(); AllSkills.Empty();
if (!SkillDataTable) if (!SkillDataTable)
{ {
UE_LOG(LogTemp, Warning, TEXT("SkillTreeComponent: No skill data table assigned!")); UE_LOG(LogTemp, Warning,
return; TEXT("SkillTreeComponent: No skill data table assigned!"));
} return;
}
// Load all skills from the data table using RowNames as IDs // Load all skills from the data table using RowNames as IDs
TArray<FName> RowNames = SkillDataTable->GetRowNames(); TArray<FName> RowNames = SkillDataTable->GetRowNames();
for (const FName& RowName : RowNames)
{
FSkillNodeData* Row = SkillDataTable->FindRow<FSkillNodeData>(
RowName, TEXT("SkillTreeComponent"));
if (Row)
{
FSkillNodeRuntime RuntimeNode;
RuntimeNode.NodeData = *Row;
RuntimeNode.CurrentState = ESkillNodeState::Locked;
for (const FName& RowName : RowNames) // Use RowName as the skill ID
{ AllSkills.Add(RowName, RuntimeNode);
FSkillNodeData* Row = SkillDataTable->FindRow<FSkillNodeData>(RowName, TEXT("SkillTreeComponent")); }
if (Row) }
{
FSkillNodeRuntime RuntimeNode;
RuntimeNode.NodeData = *Row;
RuntimeNode.CurrentState = ESkillNodeState::Locked;
RuntimeNode.bIsSelected = false;
// Use RowName as the skill ID UpdateSkillStates();
AllSkills.Add(RowName, RuntimeNode);
}
}
UpdateSkillStates(); UE_LOG(LogTemp, Log, TEXT("SkillTreeComponent: Initialized with %d skills"),
AllSkills.Num());
UE_LOG(LogTemp, Log, TEXT("SkillTreeComponent: Initialized with %d skills"), AllSkills.Num());
} }
void USkillTreeComponent::AddSkillPoints(int32 Amount) void USkillTreeComponent::AddSkillPoints(int32 Amount)
{ {
if (Amount > 0) if (Amount > 0)
{ {
AvailableSkillPoints += Amount; AvailableSkillPoints += Amount;
TotalSkillPointsEarned += Amount; TotalSkillPointsEarned += Amount;
OnSkillPointsChanged.Broadcast(AvailableSkillPoints); OnSkillPointsChanged.Broadcast(AvailableSkillPoints);
UpdateSkillStates(); UpdateSkillStates();
} }
} }
void USkillTreeComponent::RemoveSkillPoints(int32 Amount) void USkillTreeComponent::RemoveSkillPoints(int32 Amount)
{ {
if (Amount > 0) if (Amount > 0)
{ {
AvailableSkillPoints = FMath::Max(0, AvailableSkillPoints - Amount); AvailableSkillPoints = FMath::Max(0, AvailableSkillPoints - Amount);
OnSkillPointsChanged.Broadcast(AvailableSkillPoints); OnSkillPointsChanged.Broadcast(AvailableSkillPoints);
UpdateSkillStates(); UpdateSkillStates();
} }
} }
bool USkillTreeComponent::SelectSkill(FName SkillID) bool USkillTreeComponent::SelectSkill(FName SkillID)
{ {
FText ErrorMessage; FText ErrorMessage;
// Check if skill exists // Check if skill exists
if (!AllSkills.Contains(SkillID)) if (!AllSkills.Contains(SkillID))
{ {
ErrorMessage = FText::FromString(TEXT("Skill does not exist")); ErrorMessage = FText::FromString(TEXT("Skill does not exist"));
OnSelectionError.Broadcast(SkillID, ErrorMessage); OnSelectionError.Broadcast(SkillID, ErrorMessage);
return false; return false;
} }
FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID); FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID);
// Check if already purchased // Validate skill state
if (SkillNode->CurrentState == ESkillNodeState::Purchased) switch (SkillNode->CurrentState)
{ {
ErrorMessage = FText::FromString(TEXT("Skill already purchased")); case ESkillNodeState::Purchased:
OnSelectionError.Broadcast(SkillID, ErrorMessage); // Already purchased, cannot select
return false; ErrorMessage = FText::FromString(TEXT("Skill already purchased"));
} OnSelectionError.Broadcast(SkillID, ErrorMessage);
return false;
// Check if already selected case ESkillNodeState::Selected:
if (SkillNode->bIsSelected) // Already selected, cannot select again
{ ErrorMessage = FText::FromString(TEXT("Skill already selected"));
ErrorMessage = FText::FromString(TEXT("Skill already selected")); OnSelectionError.Broadcast(SkillID, ErrorMessage);
OnSelectionError.Broadcast(SkillID, ErrorMessage); return false;
return false;
}
// For locked skills, we allow selection but will validate on purchase case ESkillNodeState::Locked:
// This allows players to "plan" their build // For locked skills, we allow selection but validate prerequisites
if (SkillNode->CurrentState == ESkillNodeState::Locked) // This allows players to "plan" their build
{ bool bWouldBeAvailable = true;
// Check if prerequisites would be met with current selection for (const FName& PrereqID : SkillNode->NodeData.Prerequisites)
bool bWouldBeAvailable = true; {
for (const FName& PrereqID : SkillNode->NodeData.Prerequisites) if (!IsSkillPurchased(PrereqID) && !IsSkillSelected(PrereqID))
{ {
if (!IsSkillPurchased(PrereqID) && !IsSkillSelected(PrereqID)) bWouldBeAvailable = false;
{ break;
bWouldBeAvailable = false; }
break; }
} if (!bWouldBeAvailable)
} {
ErrorMessage = FText::FromString(TEXT(
"Prerequisites not selected. Select prerequisite skills first"));
OnSelectionError.Broadcast(SkillID, ErrorMessage);
return false;
}
break;
if (!bWouldBeAvailable) case ESkillNodeState::Available:
{ // Available skills can be selected without additional checks
ErrorMessage = FText::FromString(TEXT("Prerequisites not selected. Select prerequisite skills first.")); break;
OnSelectionError.Broadcast(SkillID, ErrorMessage); }
return false;
}
}
// Add to selection // Add to selection
SkillNode->bIsSelected = true; SkillNode->CurrentState = ESkillNodeState::Selected;
SelectedSkills.Add(SkillID); SelectedSkills.Add(SkillID);
UpdateSelectionCost(); UpdateSelectionCost();
OnSkillSelectionChanged.Broadcast(CurrentSelectionCost); OnSkillSelectionChanged.Broadcast(CurrentSelectionCost);
OnSkillStateChanged.Broadcast(SkillID); OnSkillStateChanged.Broadcast(SkillID);
return true; return true;
} }
bool USkillTreeComponent::DeselectSkill(FName SkillID) bool USkillTreeComponent::DeselectSkill(FName SkillID)
{ {
if (!AllSkills.Contains(SkillID)) if (!AllSkills.Contains(SkillID))
{ return false;
return false;
}
FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID); FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID);
if (!SkillNode->bIsSelected) if (SkillNode->CurrentState != ESkillNodeState::Selected)
{ return false;
return false;
}
// Check if any selected skills depend on this one // Check if any selected skills depend on this one
for (const FName& SelectedID : SelectedSkills) for (const FName& SelectedID : SelectedSkills)
{ {
if (SelectedID != SkillID) if (SelectedID != SkillID)
{ {
FSkillNodeRuntime* SelectedNode = AllSkills.Find(SelectedID); FSkillNodeRuntime* SelectedNode = AllSkills.Find(SelectedID);
if (SelectedNode && SelectedNode->NodeData.Prerequisites.Contains(SkillID)) if (SelectedNode
{ && SelectedNode->NodeData.Prerequisites.Contains(SkillID))
// Can't deselect if other selected skills depend on it {
FText ErrorMessage = FText::FromString(TEXT("Other selected skills depend on this one")); // Can't deselect if other selected skills depend on it
OnSelectionError.Broadcast(SkillID, ErrorMessage); FText ErrorMessage = FText::FromString(
return false; TEXT("Other selected skills depend on this one"));
} OnSelectionError.Broadcast(SkillID, ErrorMessage);
} return false;
} }
}
}
// Remove from selection // Remove from selection and update state
SkillNode->bIsSelected = false; SelectedSkills.Remove(SkillID);
SelectedSkills.Remove(SkillID); UpdateSelectionCost();
UpdateSelectionCost();
OnSkillSelectionChanged.Broadcast(CurrentSelectionCost); // Update state to either Available or Locked based on prerequisites
OnSkillStateChanged.Broadcast(SkillID); if (ArePrerequisitesMet(SkillNode->NodeData))
SkillNode->CurrentState = ESkillNodeState::Available;
else
SkillNode->CurrentState = ESkillNodeState::Locked;
return true; OnSkillSelectionChanged.Broadcast(CurrentSelectionCost);
OnSkillStateChanged.Broadcast(SkillID);
return true;
} }
void USkillTreeComponent::ClearSelection() void USkillTreeComponent::ClearSelection()
{ {
for (const FName& SkillID : SelectedSkills) for (const FName& SkillID : SelectedSkills)
{ {
if (FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID)) if (FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID))
{ {
SkillNode->bIsSelected = false; // Update state based on prerequisites
OnSkillStateChanged.Broadcast(SkillID); if (ArePrerequisitesMet(SkillNode->NodeData))
} SkillNode->CurrentState = ESkillNodeState::Available;
} else
SkillNode->CurrentState = ESkillNodeState::Locked;
OnSkillStateChanged.Broadcast(SkillID);
}
}
SelectedSkills.Empty(); SelectedSkills.Empty();
UpdateSelectionCost(); UpdateSelectionCost();
OnSkillSelectionChanged.Broadcast(CurrentSelectionCost); OnSkillSelectionChanged.Broadcast(CurrentSelectionCost);
} }
bool USkillTreeComponent::ConfirmPurchase() bool USkillTreeComponent::ConfirmPurchase()
{ {
// Validate we can afford it // Validate we can afford it
if (!CanAffordSelection()) if (!CanAffordSelection())
{ {
FText ErrorMessage = FText::FromString(TEXT("Not enough skill points")); FText ErrorMessage = FText::FromString(TEXT("Not enough skill points"));
OnSelectionError.Broadcast(NAME_None, ErrorMessage); OnSelectionError.Broadcast(NAME_None, ErrorMessage);
return false; return false;
} }
// Sort selected skills by purchase order to ensure prerequisites are purchased first // Sort selected skills by purchase order to ensure prerequisites are
TArray<FName> SortedSelection = SelectedSkills; // purchased first
SortedSelection.Sort([this](const FName& A, const FName& B) { TArray<FName> SortedSelection = SelectedSkills;
FSkillNodeRuntime* NodeA = AllSkills.Find(A); SortedSelection.Sort(
FSkillNodeRuntime* NodeB = AllSkills.Find(B); [this](const FName& A, const FName& B)
if (NodeA && NodeB) {
{ FSkillNodeRuntime* NodeA = AllSkills.Find(A);
return NodeA->NodeData.PurchaseOrder < NodeB->NodeData.PurchaseOrder; FSkillNodeRuntime* NodeB = AllSkills.Find(B);
} if (NodeA && NodeB)
return false; {
}); return NodeA->NodeData.PurchaseOrder
< NodeB->NodeData.PurchaseOrder;
}
return false;
});
// Validate all skills can be purchased in order // Validate all skills can be purchased in order
for (const FName& SkillID : SortedSelection) for (const FName& SkillID : SortedSelection)
{ {
FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID); FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID);
if (!SkillNode) if (!SkillNode)
{ continue;
continue;
}
// Check prerequisites (considering already processed skills in this batch) // Check prerequisites (considering already processed skills in this
bool bPrereqsMet = true; // batch)
for (const FName& PrereqID : SkillNode->NodeData.Prerequisites) bool bPrereqsMet = true;
{ for (const FName& PrereqID : SkillNode->NodeData.Prerequisites)
if (!IsSkillPurchased(PrereqID)) {
{ if (!IsSkillPurchased(PrereqID))
// Check if it's in our current batch and would be purchased before this {
int32 PrereqIndex = SortedSelection.IndexOfByKey(PrereqID); // Check if it's in our current batch and would be purchased
int32 CurrentIndex = SortedSelection.IndexOfByKey(SkillID); // before this
int32 PrereqIndex = SortedSelection.IndexOfByKey(PrereqID);
int32 CurrentIndex = SortedSelection.IndexOfByKey(SkillID);
if (PrereqIndex == INDEX_NONE || PrereqIndex >= CurrentIndex) if (PrereqIndex == INDEX_NONE || PrereqIndex >= CurrentIndex)
{ {
bPrereqsMet = false; bPrereqsMet = false;
break; break;
} }
} }
} }
if (!bPrereqsMet) if (!bPrereqsMet)
{ {
FText ErrorMessage = FText::Format( FText ErrorMessage = FText::Format(
FText::FromString(TEXT("Cannot purchase {0}: prerequisites not met")), FText::FromString(
SkillNode->NodeData.DisplayName TEXT("Cannot purchase {0}: prerequisites not met")),
); SkillNode->NodeData.DisplayName);
OnSelectionError.Broadcast(SkillID, ErrorMessage); OnSelectionError.Broadcast(SkillID, ErrorMessage);
return false; return false;
} }
} }
// Purchase all selected skills // Purchase all selected skills
for (const FName& SkillID : SortedSelection) for (const FName& SkillID : SortedSelection)
{ {
FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID); FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID);
if (SkillNode) if (SkillNode)
{ {
// Mark as purchased // Mark as purchased
SkillNode->CurrentState = ESkillNodeState::Purchased; SkillNode->CurrentState = ESkillNodeState::Purchased;
SkillNode->bIsSelected = false; PurchasedSkills.Add(SkillID);
PurchasedSkills.Add(SkillID);
// Apply effects // Apply effects
ApplySkillEffects(SkillNode->NodeData); ApplySkillEffects(SkillNode->NodeData);
// Broadcast purchase event // Broadcast purchase event
OnSkillPurchased.Broadcast(SkillID); OnSkillPurchased.Broadcast(SkillID);
OnSkillStateChanged.Broadcast(SkillID); OnSkillStateChanged.Broadcast(SkillID);
} }
} }
// Deduct skill points // Deduct skill points
RemoveSkillPoints(CurrentSelectionCost); RemoveSkillPoints(CurrentSelectionCost);
// Clear selection // Clear selection
SelectedSkills.Empty(); SelectedSkills.Empty();
UpdateSelectionCost(); UpdateSelectionCost();
UpdateSkillStates(); UpdateSkillStates();
OnSkillSelectionChanged.Broadcast(CurrentSelectionCost); OnSkillSelectionChanged.Broadcast(CurrentSelectionCost);
return true; return true;
} }
bool USkillTreeComponent::IsSkillPurchased(FName SkillID) const bool USkillTreeComponent::IsSkillPurchased(FName SkillID) const
{ {
return PurchasedSkills.Contains(SkillID); return PurchasedSkills.Contains(SkillID);
} }
bool USkillTreeComponent::IsSkillSelected(FName SkillID) const bool USkillTreeComponent::IsSkillSelected(FName SkillID) const
{ {
return SelectedSkills.Contains(SkillID); return SelectedSkills.Contains(SkillID);
} }
bool USkillTreeComponent::IsSkillAvailable(FName SkillID) const bool USkillTreeComponent::IsSkillAvailable(FName SkillID) const
{ {
const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID); const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID);
if (!SkillNode) if (!SkillNode)
{ return false;
return false;
}
return SkillNode->CurrentState == ESkillNodeState::Available; return SkillNode->CurrentState == ESkillNodeState::Available;
} }
bool USkillTreeComponent::CanSelectSkill(FName SkillID, FText& OutErrorMessage) const bool USkillTreeComponent::CanSelectSkill(FName SkillID,
FText& OutErrorMessage) const
{ {
const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID); const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID);
if (!SkillNode) if (!SkillNode)
{ {
OutErrorMessage = FText::FromString(TEXT("Skill does not exist")); OutErrorMessage = FText::FromString(TEXT("Skill does not exist"));
return false; return false;
} }
if (SkillNode->CurrentState == ESkillNodeState::Purchased) switch (SkillNode->CurrentState)
{ {
OutErrorMessage = FText::FromString(TEXT("Skill already purchased")); case ESkillNodeState::Purchased:
return false; OutErrorMessage = FText::FromString(TEXT("Skill already purchased"));
} return false;
if (SkillNode->bIsSelected) case ESkillNodeState::Selected:
{ OutErrorMessage = FText::FromString(TEXT("Skill already selected"));
OutErrorMessage = FText::FromString(TEXT("Skill already selected")); return false;
return false;
}
OutErrorMessage = FText::GetEmpty(); case ESkillNodeState::Locked:
return true; case ESkillNodeState::Available:
OutErrorMessage = FText::GetEmpty();
return true;
default:
OutErrorMessage = FText::FromString(TEXT("Unknown skill state"));
return false;
}
} }
ESkillNodeState USkillTreeComponent::GetSkillState(FName SkillID) const ESkillNodeState USkillTreeComponent::GetSkillState(FName SkillID) const
{ {
const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID); const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID);
if (SkillNode) if (SkillNode)
{ return SkillNode->CurrentState;
return SkillNode->CurrentState;
}
return ESkillNodeState::Locked; return ESkillNodeState::Locked;
} }
bool USkillTreeComponent::GetSkillNodeData(FName SkillID, FSkillNodeRuntime& OutNodeData) const bool USkillTreeComponent::GetSkillNodeData(FName SkillID,
FSkillNodeRuntime& OutNodeData) const
{ {
const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID); const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID);
if (SkillNode) if (SkillNode)
{ {
OutNodeData = *SkillNode; OutNodeData = *SkillNode;
return true; return true;
} }
return false; return false;
} }
TArray<FSkillNodeRuntime> USkillTreeComponent::GetAllSkillNodes() const TArray<FSkillNodeRuntime> USkillTreeComponent::GetAllSkillNodes() const
{ {
TArray<FSkillNodeRuntime> Result; TArray<FSkillNodeRuntime> Result;
for (const auto& Pair : AllSkills) for (const auto& Pair : AllSkills)
{ Result.Add(Pair.Value);
Result.Add(Pair.Value);
} return Result;
return Result;
} }
float USkillTreeComponent::GetTotalHealthBonus() const float USkillTreeComponent::GetTotalHealthBonus() const
{ {
float Total = 0.0f; float Total = 0.0f;
for (const FName& SkillID : PurchasedSkills) for (const FName& SkillID : PurchasedSkills)
{ {
if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID)) if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID))
{ Total += SkillNode->NodeData.HealthBonus;
Total += SkillNode->NodeData.HealthBonus; }
}
} return Total;
return Total;
} }
float USkillTreeComponent::GetTotalDamageBonus() const float USkillTreeComponent::GetTotalDamageBonus() const
{ {
float Total = 0.0f; float Total = 0.0f;
for (const FName& SkillID : PurchasedSkills) for (const FName& SkillID : PurchasedSkills)
{ {
if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID)) if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID))
{ Total += SkillNode->NodeData.DamageBonus;
Total += SkillNode->NodeData.DamageBonus; }
}
} return Total;
return Total;
} }
float USkillTreeComponent::GetTotalSpeedBonus() const float USkillTreeComponent::GetTotalSpeedBonus() const
{ {
float Total = 0.0f; float Total = 0.0f;
for (const FName& SkillID : PurchasedSkills) for (const FName& SkillID : PurchasedSkills)
{ {
if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID)) if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID))
{ Total += SkillNode->NodeData.SpeedBonus;
Total += SkillNode->NodeData.SpeedBonus; }
}
} return Total;
return Total;
} }
float USkillTreeComponent::GetTotalHealthBonusPercent() const float USkillTreeComponent::GetTotalHealthBonusPercent() const
{ {
float Total = 0.0f; float Total = 0.0f;
for (const FName& SkillID : PurchasedSkills) for (const FName& SkillID : PurchasedSkills)
{ {
if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID)) if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID))
{ Total += SkillNode->NodeData.HealthBonusPercent;
Total += SkillNode->NodeData.HealthBonusPercent; }
}
} return Total;
return Total;
} }
float USkillTreeComponent::GetTotalDamageBonusPercent() const float USkillTreeComponent::GetTotalDamageBonusPercent() const
{ {
float Total = 0.0f; float Total = 0.0f;
for (const FName& SkillID : PurchasedSkills) for (const FName& SkillID : PurchasedSkills)
{ {
if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID)) if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID))
{ Total += SkillNode->NodeData.DamageBonusPercent;
Total += SkillNode->NodeData.DamageBonusPercent; }
}
} return Total;
return Total;
} }
float USkillTreeComponent::GetTotalSpeedBonusPercent() const float USkillTreeComponent::GetTotalSpeedBonusPercent() const
{ {
float Total = 0.0f; float Total = 0.0f;
for (const FName& SkillID : PurchasedSkills) for (const FName& SkillID : PurchasedSkills)
{ {
if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID)) if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID))
{ Total += SkillNode->NodeData.SpeedBonusPercent;
Total += SkillNode->NodeData.SpeedBonusPercent; }
}
} return Total;
return Total;
} }
void USkillTreeComponent::ResetAllSkills() void USkillTreeComponent::ResetAllSkills()
{ {
// Refund all skill points // Refund all skill points
int32 RefundAmount = 0; int32 RefundAmount = 0;
for (const FName& SkillID : PurchasedSkills) for (const FName& SkillID : PurchasedSkills)
{ {
if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID)) if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID))
{ {
RefundAmount += SkillNode->NodeData.Cost; RefundAmount += SkillNode->NodeData.Cost;
RemoveSkillEffects(SkillNode->NodeData); RemoveSkillEffects(SkillNode->NodeData);
} }
} }
// Clear purchases // Clear purchases
PurchasedSkills.Empty(); PurchasedSkills.Empty();
// Reset all skill states // Reset all skill states
for (auto& Pair : AllSkills) for (auto& Pair : AllSkills)
{ Pair.Value.CurrentState = ESkillNodeState::Locked;
Pair.Value.CurrentState = ESkillNodeState::Locked;
Pair.Value.bIsSelected = false;
}
// Clear selection // Clear selection
SelectedSkills.Empty(); SelectedSkills.Empty();
CurrentSelectionCost = 0; CurrentSelectionCost = 0;
// Add refunded points // Add refunded points
AddSkillPoints(RefundAmount); AddSkillPoints(RefundAmount);
// Update states // Update states
UpdateSkillStates(); UpdateSkillStates();
// Broadcast events // Broadcast events
OnSkillSelectionChanged.Broadcast(CurrentSelectionCost); OnSkillSelectionChanged.Broadcast(CurrentSelectionCost);
} }
void USkillTreeComponent::UpdateSkillStates() void USkillTreeComponent::UpdateSkillStates()
{ {
for (auto& Pair : AllSkills) for (auto& Pair : AllSkills)
{ {
FSkillNodeRuntime& SkillNode = Pair.Value; FSkillNodeRuntime& SkillNode = Pair.Value;
// Skip if already purchased // Only update Locked and Available states based on prerequisites
if (SkillNode.CurrentState == ESkillNodeState::Purchased) // Purchased and Selected states should not be changed
{ switch (SkillNode.CurrentState)
continue; {
} case ESkillNodeState::Purchased:
case ESkillNodeState::Selected:
// Don't change these states
continue;
// Check if prerequisites are met case ESkillNodeState::Locked:
if (ArePrerequisitesMet(SkillNode.NodeData)) case ESkillNodeState::Available:
{ // Update state based on prerequisites
SkillNode.CurrentState = SkillNode.bIsSelected ? if (ArePrerequisitesMet(SkillNode.NodeData))
ESkillNodeState::Selected : ESkillNodeState::Available; {
} SkillNode.CurrentState = ESkillNodeState::Available;
else }
{ else
SkillNode.CurrentState = SkillNode.bIsSelected ? {
ESkillNodeState::Selected : ESkillNodeState::Locked; SkillNode.CurrentState = ESkillNodeState::Locked;
} }
} break;
}
}
} }
void USkillTreeComponent::UpdateSelectionCost() void USkillTreeComponent::UpdateSelectionCost()
{ {
CurrentSelectionCost = 0; CurrentSelectionCost = 0;
for (const FName& SkillID : SelectedSkills) for (const FName& SkillID : SelectedSkills)
{ {
if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID)) if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID))
{ {
CurrentSelectionCost += SkillNode->NodeData.Cost; CurrentSelectionCost += SkillNode->NodeData.Cost;
} }
} }
} }
bool USkillTreeComponent::ArePrerequisitesMet(const FSkillNodeData& SkillData) const bool USkillTreeComponent::ArePrerequisitesMet(
const FSkillNodeData& SkillData) const
{ {
for (const FName& PrereqID : SkillData.Prerequisites) for (const FName& PrereqID : SkillData.Prerequisites)
{ {
if (!IsSkillPurchased(PrereqID)) if (!IsSkillPurchased(PrereqID))
{ {
return false; return false;
} }
} }
return true; return true;
} }
bool USkillTreeComponent::HasChildrenPurchased(FName SkillID) const bool USkillTreeComponent::HasChildrenPurchased(FName SkillID) const
{ {
for (const auto& Pair : AllSkills) for (const auto& Pair : AllSkills)
{ {
// Pair.Key is the RowName (SkillID) // Pair.Key is the RowName (SkillID)
if (Pair.Value.NodeData.Prerequisites.Contains(SkillID) && if (Pair.Value.NodeData.Prerequisites.Contains(SkillID)
IsSkillPurchased(Pair.Key)) && IsSkillPurchased(Pair.Key))
{ {
return true; return true;
} }
} }
return false; return false;
} }
void USkillTreeComponent::ApplySkillEffects(const FSkillNodeData& SkillData) void USkillTreeComponent::ApplySkillEffects(const FSkillNodeData& SkillData)
{ {
// This is where you would apply the actual skill effects to the character // This is where you would apply the actual skill effects to the character
// For now, we just log it // For now, we just log it
UE_LOG(LogTemp, Log, TEXT("Applied skill effects for: %s"), *SkillData.DisplayName.ToString()); UE_LOG(LogTemp, Log, TEXT("Applied skill effects for: %s"),
*SkillData.DisplayName.ToString());
} }
void USkillTreeComponent::RemoveSkillEffects(const FSkillNodeData& SkillData) void USkillTreeComponent::RemoveSkillEffects(const FSkillNodeData& SkillData)
{ {
// This is where you would remove skill effects from the character // This is where you would remove skill effects from the character
// For now, we just log it // For now, we just log it
UE_LOG(LogTemp, Log, TEXT("Removed skill effects for: %s"), *SkillData.DisplayName.ToString()); UE_LOG(LogTemp, Log, TEXT("Removed skill effects for: %s"),
} *SkillData.DisplayName.ToString());
}

View File

@@ -8,97 +8,94 @@
UENUM(BlueprintType) UENUM(BlueprintType)
enum class ESkillNodeState : uint8 enum class ESkillNodeState : uint8
{ {
Locked UMETA(DisplayName = "Locked"), // Can't be purchased yet (missing prerequisites) // Can't be purchased yet (missing prerequisites)
Available UMETA(DisplayName = "Available"), // Can be purchased Locked UMETA(DisplayName = "Locked"),
Selected UMETA(DisplayName = "Selected"), // Currently selected for purchase
Purchased UMETA(DisplayName = "Purchased") // Already owned // Can be purchased
Available UMETA(DisplayName = "Available"),
// Currently selected for purchase
Selected UMETA(DisplayName = "Selected"),
// Already owned
Purchased UMETA(DisplayName = "Purchased")
}; };
// Struct for individual skill node data // Struct for individual skill node data
// Note: RowName in the DataTable serves as the unique SkillID
USTRUCT(BlueprintType) USTRUCT(BlueprintType)
struct FSkillNodeData : public FTableRowBase struct FSkillNodeData : public FTableRowBase
{ {
GENERATED_BODY() GENERATED_BODY()
// Display name for UI // RowName will be used as SkillID
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill")
FText DisplayName;
// Description of what the skill does // Display name for UI
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill")
FText Description; FText DisplayName;
// Icon for the skill // Description of what the skill does
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill")
class UTexture2D* Icon; FText Description;
// Skill point cost // Icon for the skill
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill")
int32 Cost; class UTexture2D* Icon;
// Purchase order priority (lower = buy first, used when confirming multiple skills) // Skill point cost
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill")
int32 PurchaseOrder; int32 Cost;
// Prerequisites - must have these skills first (RowNames of required skills) // Purchase order priority
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill") // (lower = buy first, used when confirming multiple skills)
TArray<FName> Prerequisites; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill")
int32 PurchaseOrder;
// Effects this skill provides (actual stat bonuses) // Prerequisites - must have these skills first
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill")
float HealthBonus; TArray<FName> Prerequisites;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects") // Effects this skill provides
float DamageBonus; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
float HealthBonus;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
float DamageBonus;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
float SpeedBonus;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
float SpeedBonus; float HealthBonusPercent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
float DamageBonusPercent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
float SpeedBonusPercent;
// Percentage bonuses (multiplicative) FSkillNodeData()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects") {
float HealthBonusPercent; DisplayName = FText::GetEmpty();
Description = FText::GetEmpty();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects") Icon = nullptr;
float DamageBonusPercent; Cost = 1;
PurchaseOrder = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects") HealthBonus = 0.0f;
float SpeedBonusPercent; DamageBonus = 0.0f;
SpeedBonus = 0.0f;
FSkillNodeData() HealthBonusPercent = 0.0f;
{ DamageBonusPercent = 0.0f;
DisplayName = FText::GetEmpty(); SpeedBonusPercent = 0.0f;
Description = FText::GetEmpty(); }
Icon = nullptr;
Cost = 1;
PurchaseOrder = 0;
HealthBonus = 0.0f;
DamageBonus = 0.0f;
SpeedBonus = 0.0f;
HealthBonusPercent = 0.0f;
DamageBonusPercent = 0.0f;
SpeedBonusPercent = 0.0f;
}
}; };
// Runtime struct for tracking skill state // Runtime struct for tracking skill state
USTRUCT(BlueprintType) USTRUCT(BlueprintType)
struct FSkillNodeRuntime struct FSkillNodeRuntime
{ {
GENERATED_BODY() GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Skill") UPROPERTY(BlueprintReadOnly, Category = "Skill")
FSkillNodeData NodeData; FSkillNodeData NodeData;
UPROPERTY(BlueprintReadOnly, Category = "Skill") UPROPERTY(BlueprintReadOnly, Category = "Skill")
ESkillNodeState CurrentState; ESkillNodeState CurrentState;
UPROPERTY(BlueprintReadOnly, Category = "Skill") FSkillNodeRuntime() { CurrentState = ESkillNodeState::Locked; }
bool bIsSelected; };
FSkillNodeRuntime()
{
CurrentState = ESkillNodeState::Locked;
bIsSelected = false;
}
};