574 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			574 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| #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<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;
 | |
| 
 | |
| 			// 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<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;
 | |
| 			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<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;
 | |
| 
 | |
| 	// 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());
 | |
| }
 |