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