#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(); PurchasedSkills.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); } } // Auto-purchase starting skills (these don't cost skill points) for (auto& Pair : AllSkills) { const FName& SkillID = Pair.Key; FSkillNodeRuntime& SkillNode = Pair.Value; if (SkillNode.NodeData.bStartingSkill) { // Mark as purchased SkillNode.CurrentState = ESkillNodeState::Purchased; PurchasedSkills.Add(SkillID); // Apply skill effects ApplySkillEffects(SkillNode.NodeData); // Broadcast purchase event OnSkillPurchased.Broadcast(SkillID); UE_LOG(LogTemp, Log, TEXT("SkillTreeComponent: Auto-purchased starting skill: %s"), *SkillNode.NodeData.DisplayName.ToString()); } } UpdateSkillStates(); UE_LOG(LogTemp, Log, TEXT("SkillTreeComponent: Initialized with %d skills (%d starting)"), AllSkills.Num(), PurchasedSkills.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; if (!AllSkills.Contains(SkillID)) { ErrorMessage = FText::FromString(TEXT("Skill does not exist")); OnSelectionError.Broadcast(SkillID, ErrorMessage); return false; } FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID); switch (SkillNode->CurrentState) { case ESkillNodeState::Purchased: ErrorMessage = FText::FromString(TEXT("Skill already purchased")); OnSelectionError.Broadcast(SkillID, ErrorMessage); return false; case ESkillNodeState::Selected: ErrorMessage = FText::FromString(TEXT("Skill already selected")); OnSelectionError.Broadcast(SkillID, ErrorMessage); return false; case ESkillNodeState::Locked: ErrorMessage = FText::FromString(TEXT("Prerequisites not met or not enough skill points")); OnSelectionError.Broadcast(SkillID, ErrorMessage); return false; case ESkillNodeState::Available: // Double-check affordability (should already be Available only if affordable) if ((AvailableSkillPoints - CurrentSelectionCost) < SkillNode->NodeData.Cost) { ErrorMessage = FText::FromString(TEXT("Not enough skill points")); OnSelectionError.Broadcast(SkillID, ErrorMessage); return false; } break; } SkillNode->CurrentState = ESkillNodeState::Selected; SelectedSkills.Add(SkillID); UpdateSelectionCost(); UpdateSkillStates(); 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; CascadeDeselectDependents(SkillID); SelectedSkills.Remove(SkillID); UpdateSelectionCost(); UpdateSkillStates(); OnSkillSelectionChanged.Broadcast(CurrentSelectionCost); return true; } void USkillTreeComponent::ClearSelection() { SelectedSkills.Empty(); UpdateSelectionCost(); UpdateSkillStates(); 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: OutErrorMessage = FText::FromString(TEXT("Prerequisites not met or not enough skill points")); return false; case ESkillNodeState::Available: // Double-check affordability if ((AvailableSkillPoints - CurrentSelectionCost) < SkillNode->NodeData.Cost) { OutErrorMessage = FText::FromString(TEXT("Not enough skill points")); return false; } 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 skill points (but not for starting skills) int32 RefundAmount = 0; for (const FName& SkillID : PurchasedSkills) { if (const FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID)) { // Remove skill effects for all purchased skills RemoveSkillEffects(SkillNode->NodeData); // Only refund cost for non-starting skills if (!SkillNode->NodeData.bStartingSkill) { RefundAmount += SkillNode->NodeData.Cost; } } } // 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 if (RefundAmount > 0) { AddSkillPoints(RefundAmount); } // Re-purchase starting skills for (auto& Pair : AllSkills) { const FName& SkillID = Pair.Key; FSkillNodeRuntime& SkillNode = Pair.Value; if (SkillNode.NodeData.bStartingSkill) { // Mark as purchased SkillNode.CurrentState = ESkillNodeState::Purchased; PurchasedSkills.Add(SkillID); // Apply skill effects ApplySkillEffects(SkillNode.NodeData); // Broadcast purchase event OnSkillPurchased.Broadcast(SkillID); } } // Update states UpdateSkillStates(); // Broadcast events OnSkillSelectionChanged.Broadcast(CurrentSelectionCost); UE_LOG(LogTemp, Log, TEXT("SkillTreeComponent: Reset all skills. Refunded %d points. Re-purchased %d starting skills."), RefundAmount, PurchasedSkills.Num()); } void USkillTreeComponent::UpdateSkillStates() { for (auto& Pair : AllSkills) { const FName& SkillID = Pair.Key; FSkillNodeRuntime& SkillNode = Pair.Value; switch (SkillNode.CurrentState) { case ESkillNodeState::Purchased: // Never change purchased state continue; case ESkillNodeState::Selected: // If marked as Selected but NOT in SelectedSkills array, it was deselected if (!SelectedSkills.Contains(SkillID)) { // Check if it can be Available (prerequisites + affordability) bool bPrereqsMet = ArePrerequisitesMet(SkillNode.NodeData, true); bool bCanAfford = (AvailableSkillPoints - CurrentSelectionCost) >= SkillNode.NodeData.Cost; ESkillNodeState NewState = (bPrereqsMet && bCanAfford) ? ESkillNodeState::Available : ESkillNodeState::Locked; SkillNode.CurrentState = NewState; OnSkillStateChanged.Broadcast(SkillID); } // Keep it Selected break; case ESkillNodeState::Locked: case ESkillNodeState::Available: { // Skill is Available only if: // 1. Prerequisites are met (purchased or selected) // 2. Player has enough skill points to afford it bool bPrereqsMet = ArePrerequisitesMet(SkillNode.NodeData, true); bool bCanAfford = (AvailableSkillPoints - CurrentSelectionCost) >= SkillNode.NodeData.Cost; ESkillNodeState NewState = (bPrereqsMet && bCanAfford) ? ESkillNodeState::Available : ESkillNodeState::Locked; // Only broadcast if state actually changed if (SkillNode.CurrentState != NewState) { SkillNode.CurrentState = NewState; OnSkillStateChanged.Broadcast(SkillID); } } 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, bool bIncludeSelected) const { for (const FName& PrereqID : SkillData.Prerequisites) { // Check if prerequisite is purchased if (IsSkillPurchased(PrereqID)) continue; // If including selected, also check if prerequisite is selected if (bIncludeSelected && IsSkillSelected(PrereqID)) continue; // Prerequisite not met 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::CascadeDeselectDependents(FName SkillID) { // Find all selected skills that depend on this skill TArray ToDeselect; for (const FName& SelectedID : SelectedSkills) { if (SelectedID == SkillID) continue; FSkillNodeRuntime* SelectedNode = AllSkills.Find(SelectedID); if (SelectedNode && SelectedNode->NodeData.Prerequisites.Contains(SkillID)) { ToDeselect.Add(SelectedID); } } // Recursively deselect dependent skills for (const FName& DependentID : ToDeselect) { CascadeDeselectDependents(DependentID); SelectedSkills.Remove(DependentID); } } 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()); }