feat: reset button and only available if affordable
This commit is contained in:
		
							
								
								
									
										
											BIN
										
									
								
								Content/UI/WBP_SkillNode.uasset
									 (Stored with Git LFS)
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Content/UI/WBP_SkillNode.uasset
									 (Stored with Git LFS)
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Content/UI/WBP_SkillTree.uasset
									 (Stored with Git LFS)
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Content/UI/WBP_SkillTree.uasset
									 (Stored with Git LFS)
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -75,7 +75,6 @@ bool USkillTreeComponent::SelectSkill(FName SkillID) | ||||
| { | ||||
| 	FText ErrorMessage; | ||||
|  | ||||
| 	// Check if skill exists | ||||
| 	if (!AllSkills.Contains(SkillID)) | ||||
| 	{ | ||||
| 		ErrorMessage = FText::FromString(TEXT("Skill does not exist")); | ||||
| @@ -84,55 +83,40 @@ bool USkillTreeComponent::SelectSkill(FName SkillID) | ||||
| 	} | ||||
|  | ||||
| 	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; | ||||
| 		ErrorMessage = FText::FromString(TEXT("Prerequisites not met or not enough skill points")); | ||||
| 		OnSelectionError.Broadcast(SkillID, ErrorMessage); | ||||
| 		return false; | ||||
|  | ||||
| 	case ESkillNodeState::Available: | ||||
| 		// Available skills can be selected without additional checks | ||||
| 		// 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; | ||||
| 	} | ||||
|  | ||||
| 	// Add to selection | ||||
| 	SkillNode->CurrentState = ESkillNodeState::Selected; | ||||
| 	SelectedSkills.Add(SkillID); | ||||
| 	UpdateSelectionCost(); | ||||
|  | ||||
| 	UpdateSkillStates(); | ||||
|  | ||||
| 	OnSkillSelectionChanged.Broadcast(CurrentSelectionCost); | ||||
| 	OnSkillStateChanged.Broadcast(SkillID); | ||||
|  | ||||
| @@ -149,57 +133,24 @@ bool USkillTreeComponent::DeselectSkill(FName 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; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	CascadeDeselectDependents(SkillID); | ||||
|  | ||||
| 	// 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; | ||||
|  | ||||
| 	UpdateSkillStates(); | ||||
| 	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(); | ||||
|  | ||||
| 	UpdateSkillStates(); | ||||
|  | ||||
| 	OnSkillSelectionChanged.Broadcast(CurrentSelectionCost); | ||||
| } | ||||
|  | ||||
| @@ -339,7 +290,16 @@ bool USkillTreeComponent::CanSelectSkill(FName	SkillID, | ||||
| 		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; | ||||
|  | ||||
| @@ -493,20 +453,40 @@ void USkillTreeComponent::UpdateSkillStates() | ||||
| 		const FName& SkillID = Pair.Key; | ||||
| 		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 | ||||
| 			// 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: | ||||
| 			{ | ||||
| 				// Calculate new state based on prerequisites | ||||
| 				ESkillNodeState NewState = ArePrerequisitesMet(SkillNode.NodeData) | ||||
| 				// 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; | ||||
|  | ||||
| @@ -535,14 +515,20 @@ void USkillTreeComponent::UpdateSelectionCost() | ||||
| } | ||||
|  | ||||
| bool USkillTreeComponent::ArePrerequisitesMet( | ||||
| 	const FSkillNodeData& SkillData) const | ||||
| 	const FSkillNodeData& SkillData, bool bIncludeSelected) const | ||||
| { | ||||
| 	for (const FName& PrereqID : SkillData.Prerequisites) | ||||
| 	{ | ||||
| 		if (!IsSkillPurchased(PrereqID)) | ||||
| 		{ | ||||
| 			return false; | ||||
| 		} | ||||
| 		// 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; | ||||
| } | ||||
| @@ -561,6 +547,30 @@ bool USkillTreeComponent::HasChildrenPurchased(FName SkillID) const | ||||
| 	return false; | ||||
| } | ||||
|  | ||||
| void USkillTreeComponent::CascadeDeselectDependents(FName SkillID) | ||||
| { | ||||
| 	// Find all selected skills that depend on this skill | ||||
| 	TArray<FName> 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 | ||||
|   | ||||
| @@ -24,19 +24,18 @@ protected: | ||||
|  | ||||
|     // Current skill points available | ||||
|     UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Skill Points", SaveGame) | ||||
|     int32 AvailableSkillPoints; | ||||
|  | ||||
|     int32 AvailableSkillPoints = 0; | ||||
|     // Total skill points earned | ||||
|     UPROPERTY(BlueprintReadOnly, Category = "Skill Points", SaveGame) | ||||
|     int32 TotalSkillPointsEarned; | ||||
|     int32 TotalSkillPointsEarned = 0; | ||||
|  | ||||
|     // Data table containing all skill definitions | ||||
|     UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Skill Tree") | ||||
|     class UDataTable* SkillDataTable; | ||||
|     class UDataTable* SkillDataTable = nullptr; | ||||
|  | ||||
|     // All skills loaded from the data table | ||||
|     UPROPERTY(BlueprintReadOnly, Category = "Skill Tree") | ||||
|     TMap<FName, FSkillNodeRuntime> AllSkills; | ||||
|     TMap<FName, FSkillNodeRuntime> AllSkills = {}; | ||||
|  | ||||
|     // Currently purchased skills | ||||
|     UPROPERTY(BlueprintReadOnly, Category = "Skill Tree", SaveGame) | ||||
| @@ -93,10 +92,8 @@ public: | ||||
|     // Skill State Queries | ||||
|     UFUNCTION(BlueprintPure, Category = "Skill State") | ||||
|     bool IsSkillPurchased(FName SkillID) const; | ||||
|  | ||||
|     UFUNCTION(BlueprintPure, Category = "Skill State") | ||||
|     bool IsSkillSelected(FName SkillID) const; | ||||
|  | ||||
|     UFUNCTION(BlueprintPure, Category = "Skill State") | ||||
|     bool IsSkillAvailable(FName SkillID) const; | ||||
|  | ||||
| @@ -116,19 +113,15 @@ public: | ||||
|     // Calculate total bonuses from purchased skills | ||||
|     UFUNCTION(BlueprintPure, Category = "Skill Bonuses") | ||||
|     float GetTotalHealthBonus() const; | ||||
|  | ||||
|     UFUNCTION(BlueprintPure, Category = "Skill Bonuses") | ||||
|     float GetTotalDamageBonus() const; | ||||
|  | ||||
|     UFUNCTION(BlueprintPure, Category = "Skill Bonuses") | ||||
|     float GetTotalSpeedBonus() const; | ||||
| 	 | ||||
|     UFUNCTION(BlueprintPure, Category = "Skill Bonuses") | ||||
|     float GetTotalHealthBonusPercent() const; | ||||
|  | ||||
|     UFUNCTION(BlueprintPure, Category = "Skill Bonuses") | ||||
|     float GetTotalDamageBonusPercent() const; | ||||
|  | ||||
|     UFUNCTION(BlueprintPure, Category = "Skill Bonuses") | ||||
|     float GetTotalSpeedBonusPercent() const; | ||||
|  | ||||
| @@ -139,16 +132,12 @@ public: | ||||
|     // Events | ||||
|     UPROPERTY(BlueprintAssignable, Category = "Skill Events") | ||||
|     FOnSkillPointsChanged OnSkillPointsChanged; | ||||
|  | ||||
|     UPROPERTY(BlueprintAssignable, Category = "Skill Events") | ||||
|     FOnSkillPurchased OnSkillPurchased; | ||||
|  | ||||
|     UPROPERTY(BlueprintAssignable, Category = "Skill Events") | ||||
|     FOnSkillSelectionChanged OnSkillSelectionChanged; | ||||
|  | ||||
|     UPROPERTY(BlueprintAssignable, Category = "Skill Events") | ||||
|     FOnSkillStateChanged OnSkillStateChanged; | ||||
|  | ||||
|     UPROPERTY(BlueprintAssignable, Category = "Skill Events") | ||||
|     FOnSelectionError OnSelectionError; | ||||
|  | ||||
| @@ -156,8 +145,9 @@ private: | ||||
|     // Internal helper functions | ||||
|     void UpdateSkillStates(); | ||||
|     void UpdateSelectionCost(); | ||||
|     bool ArePrerequisitesMet(const FSkillNodeData& SkillData) const; | ||||
|     bool ArePrerequisitesMet(const FSkillNodeData& SkillData, bool bIncludeSelected = false) const; | ||||
|     bool HasChildrenPurchased(FName SkillID) const; | ||||
|     void ApplySkillEffects(const FSkillNodeData& SkillData); | ||||
|     void RemoveSkillEffects(const FSkillNodeData& SkillData); | ||||
|     void CascadeDeselectDependents(FName SkillID); | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user