557 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			557 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| #include "SkillTreeComponent.h"
 | |
| #include "Engine/DataTable.h"
 | |
| 
 | |
| USkillTreeComponent::USkillTreeComponent()
 | |
| {
 | |
|     PrimaryComponentTick.bCanEverTick = false;
 | |
| 
 | |
|     AvailableSkillPoints = 5; // Start with 5 skill points for testing
 | |
|     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<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;
 | |
|             RuntimeNode.bIsSelected = false;
 | |
| 
 | |
|             // 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);
 | |
| 
 | |
|     // Check if already purchased
 | |
|     if (SkillNode->CurrentState == ESkillNodeState::Purchased)
 | |
|     {
 | |
|         ErrorMessage = FText::FromString(TEXT("Skill already purchased"));
 | |
|         OnSelectionError.Broadcast(SkillID, ErrorMessage);
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     // Check if already selected
 | |
|     if (SkillNode->bIsSelected)
 | |
|     {
 | |
|         ErrorMessage = FText::FromString(TEXT("Skill already selected"));
 | |
|         OnSelectionError.Broadcast(SkillID, ErrorMessage);
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     // For locked skills, we allow selection but will validate on purchase
 | |
|     // This allows players to "plan" their build
 | |
|     if (SkillNode->CurrentState == ESkillNodeState::Locked)
 | |
|     {
 | |
|         // Check if prerequisites would be met with current selection
 | |
|         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;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // Add to selection
 | |
|     SkillNode->bIsSelected = true;
 | |
|     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->bIsSelected)
 | |
|     {
 | |
|         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
 | |
|     SkillNode->bIsSelected = false;
 | |
|     SelectedSkills.Remove(SkillID);
 | |
|     UpdateSelectionCost();
 | |
| 
 | |
|     OnSkillSelectionChanged.Broadcast(CurrentSelectionCost);
 | |
|     OnSkillStateChanged.Broadcast(SkillID);
 | |
| 
 | |
|     return true;
 | |
| }
 | |
| 
 | |
| void USkillTreeComponent::ClearSelection()
 | |
| {
 | |
|     for (const FName& SkillID : SelectedSkills)
 | |
|     {
 | |
|         if (FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID))
 | |
|         {
 | |
|             SkillNode->bIsSelected = false;
 | |
|             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<FName> 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;
 | |
|             SkillNode->bIsSelected = false;
 | |
|             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;
 | |
|     }
 | |
| 
 | |
|     if (SkillNode->CurrentState == ESkillNodeState::Purchased)
 | |
|     {
 | |
|         OutErrorMessage = FText::FromString(TEXT("Skill already purchased"));
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     if (SkillNode->bIsSelected)
 | |
|     {
 | |
|         OutErrorMessage = FText::FromString(TEXT("Skill already selected"));
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     OutErrorMessage = FText::GetEmpty();
 | |
|     return true;
 | |
| }
 | |
| 
 | |
| 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<FSkillNodeRuntime> USkillTreeComponent::GetAllSkillNodes() const
 | |
| {
 | |
|     TArray<FSkillNodeRuntime> 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;
 | |
|         Pair.Value.bIsSelected = false;
 | |
|     }
 | |
| 
 | |
|     // 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;
 | |
| 
 | |
|         // Skip if already purchased
 | |
|         if (SkillNode.CurrentState == ESkillNodeState::Purchased)
 | |
|         {
 | |
|             continue;
 | |
|         }
 | |
| 
 | |
|         // Check if prerequisites are met
 | |
|         if (ArePrerequisitesMet(SkillNode.NodeData))
 | |
|         {
 | |
|             SkillNode.CurrentState = SkillNode.bIsSelected ?
 | |
|                 ESkillNodeState::Selected : ESkillNodeState::Available;
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             SkillNode.CurrentState = SkillNode.bIsSelected ?
 | |
|                 ESkillNodeState::Selected : ESkillNodeState::Locked;
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| 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());
 | |
| } |