From 2db79670f654f4f1cdaef06f05546a0ef2294241 Mon Sep 17 00:00:00 2001 From: Daniel Poveda Date: Sun, 12 Oct 2025 16:39:57 +0200 Subject: [PATCH] feat: reset button and only available if affordable --- Content/UI/WBP_SkillNode.uasset | 4 +- Content/UI/WBP_SkillTree.uasset | 4 +- .../UTAD_UI/SkillTree/SkillTreeComponent.cpp | 166 ++++++++++-------- Source/UTAD_UI/SkillTree/SkillTreeComponent.h | 24 +-- 4 files changed, 99 insertions(+), 99 deletions(-) diff --git a/Content/UI/WBP_SkillNode.uasset b/Content/UI/WBP_SkillNode.uasset index 57bdcf8..3fd330f 100644 --- a/Content/UI/WBP_SkillNode.uasset +++ b/Content/UI/WBP_SkillNode.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86232ff6d4df467ea2f4ab3550e2b037aac5882ba25378a8d6f394a930dc7750 -size 280472 +oid sha256:efe30ed48e2033040949fbc26a3a952a202388e7e4a1d84ee6e9692ce7e5ebce +size 280603 diff --git a/Content/UI/WBP_SkillTree.uasset b/Content/UI/WBP_SkillTree.uasset index db9d253..1b72776 100644 --- a/Content/UI/WBP_SkillTree.uasset +++ b/Content/UI/WBP_SkillTree.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e737788902d1ef5934994b048fa76e8ced52786fd3fcd276c514b2f030c321d -size 289016 +oid sha256:039cb822eeefa69227a75ec6ca3b17f174a8b8d68bdc2c987bf078f7fea40cad +size 316746 diff --git a/Source/UTAD_UI/SkillTree/SkillTreeComponent.cpp b/Source/UTAD_UI/SkillTree/SkillTreeComponent.cpp index b3d2819..c0afa0f 100644 --- a/Source/UTAD_UI/SkillTree/SkillTreeComponent.cpp +++ b/Source/UTAD_UI/SkillTree/SkillTreeComponent.cpp @@ -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 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 @@ -575,4 +585,4 @@ void USkillTreeComponent::RemoveSkillEffects(const FSkillNodeData& SkillData) // For now, we just log it UE_LOG(LogTemp, Log, TEXT("Removed skill effects for: %s"), *SkillData.DisplayName.ToString()); -} +} \ No newline at end of file diff --git a/Source/UTAD_UI/SkillTree/SkillTreeComponent.h b/Source/UTAD_UI/SkillTree/SkillTreeComponent.h index 14fbc91..2990d9f 100644 --- a/Source/UTAD_UI/SkillTree/SkillTreeComponent.h +++ b/Source/UTAD_UI/SkillTree/SkillTreeComponent.h @@ -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 AllSkills; + TMap 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); }; \ No newline at end of file