feat: reset button and only available if affordable

This commit is contained in:
2025-10-12 16:39:57 +02:00
parent 768ecf5ee3
commit 2db79670f6
4 changed files with 99 additions and 99 deletions

BIN
Content/UI/WBP_SkillNode.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Content/UI/WBP_SkillTree.uasset (Stored with Git LFS)

Binary file not shown.

View File

@@ -75,7 +75,6 @@ bool USkillTreeComponent::SelectSkill(FName SkillID)
{ {
FText ErrorMessage; FText ErrorMessage;
// Check if skill exists
if (!AllSkills.Contains(SkillID)) if (!AllSkills.Contains(SkillID))
{ {
ErrorMessage = FText::FromString(TEXT("Skill does not exist")); ErrorMessage = FText::FromString(TEXT("Skill does not exist"));
@@ -84,55 +83,40 @@ bool USkillTreeComponent::SelectSkill(FName SkillID)
} }
FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID); FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID);
// Validate skill state
switch (SkillNode->CurrentState) switch (SkillNode->CurrentState)
{ {
case ESkillNodeState::Purchased: case ESkillNodeState::Purchased:
// Already purchased, cannot select
ErrorMessage = FText::FromString(TEXT("Skill already purchased")); ErrorMessage = FText::FromString(TEXT("Skill already purchased"));
OnSelectionError.Broadcast(SkillID, ErrorMessage); OnSelectionError.Broadcast(SkillID, ErrorMessage);
return false; return false;
case ESkillNodeState::Selected: case ESkillNodeState::Selected:
// Already selected, cannot select again
ErrorMessage = FText::FromString(TEXT("Skill already selected")); ErrorMessage = FText::FromString(TEXT("Skill already selected"));
OnSelectionError.Broadcast(SkillID, ErrorMessage); OnSelectionError.Broadcast(SkillID, ErrorMessage);
return false; return false;
case ESkillNodeState::Locked: case ESkillNodeState::Locked:
// For locked skills, we allow selection but validate prerequisites ErrorMessage = FText::FromString(TEXT("Prerequisites not met or not enough skill points"));
// This allows players to "plan" their build 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)
{ {
bool bWouldBeAvailable = true; ErrorMessage = FText::FromString(TEXT("Not enough skill points"));
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); OnSelectionError.Broadcast(SkillID, ErrorMessage);
return false; return false;
} }
}
break;
case ESkillNodeState::Available:
// Available skills can be selected without additional checks
break; break;
} }
// Add to selection
SkillNode->CurrentState = ESkillNodeState::Selected; SkillNode->CurrentState = ESkillNodeState::Selected;
SelectedSkills.Add(SkillID); SelectedSkills.Add(SkillID);
UpdateSelectionCost(); UpdateSelectionCost();
UpdateSkillStates();
OnSkillSelectionChanged.Broadcast(CurrentSelectionCost); OnSkillSelectionChanged.Broadcast(CurrentSelectionCost);
OnSkillStateChanged.Broadcast(SkillID); OnSkillStateChanged.Broadcast(SkillID);
@@ -149,57 +133,24 @@ bool USkillTreeComponent::DeselectSkill(FName SkillID)
if (SkillNode->CurrentState != ESkillNodeState::Selected) if (SkillNode->CurrentState != ESkillNodeState::Selected)
return false; return false;
// Check if any selected skills depend on this one CascadeDeselectDependents(SkillID);
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); SelectedSkills.Remove(SkillID);
UpdateSelectionCost(); UpdateSelectionCost();
// Update state to either Available or Locked based on prerequisites UpdateSkillStates();
if (ArePrerequisitesMet(SkillNode->NodeData))
SkillNode->CurrentState = ESkillNodeState::Available;
else
SkillNode->CurrentState = ESkillNodeState::Locked;
OnSkillSelectionChanged.Broadcast(CurrentSelectionCost); OnSkillSelectionChanged.Broadcast(CurrentSelectionCost);
OnSkillStateChanged.Broadcast(SkillID);
return true; return true;
} }
void USkillTreeComponent::ClearSelection() 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(); SelectedSkills.Empty();
UpdateSelectionCost(); UpdateSelectionCost();
UpdateSkillStates();
OnSkillSelectionChanged.Broadcast(CurrentSelectionCost); OnSkillSelectionChanged.Broadcast(CurrentSelectionCost);
} }
@@ -339,7 +290,16 @@ bool USkillTreeComponent::CanSelectSkill(FName SkillID,
return false; return false;
case ESkillNodeState::Locked: case ESkillNodeState::Locked:
OutErrorMessage = FText::FromString(TEXT("Prerequisites not met or not enough skill points"));
return false;
case ESkillNodeState::Available: 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(); OutErrorMessage = FText::GetEmpty();
return true; return true;
@@ -493,20 +453,40 @@ void USkillTreeComponent::UpdateSkillStates()
const FName& SkillID = Pair.Key; const FName& SkillID = Pair.Key;
FSkillNodeRuntime& SkillNode = Pair.Value; FSkillNodeRuntime& SkillNode = Pair.Value;
// Only update Locked and Available states based on prerequisites
// Purchased and Selected states should not be changed
switch (SkillNode.CurrentState) switch (SkillNode.CurrentState)
{ {
case ESkillNodeState::Purchased: case ESkillNodeState::Purchased:
case ESkillNodeState::Selected: // Never change purchased state
// Don't change these states
continue; 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::Locked:
case ESkillNodeState::Available: case ESkillNodeState::Available:
{ {
// Calculate new state based on prerequisites // Skill is Available only if:
ESkillNodeState NewState = ArePrerequisitesMet(SkillNode.NodeData) // 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::Available
: ESkillNodeState::Locked; : ESkillNodeState::Locked;
@@ -535,15 +515,21 @@ void USkillTreeComponent::UpdateSelectionCost()
} }
bool USkillTreeComponent::ArePrerequisitesMet( bool USkillTreeComponent::ArePrerequisitesMet(
const FSkillNodeData& SkillData) const const FSkillNodeData& SkillData, bool bIncludeSelected) const
{ {
for (const FName& PrereqID : SkillData.Prerequisites) for (const FName& PrereqID : SkillData.Prerequisites)
{ {
if (!IsSkillPurchased(PrereqID)) // 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 false;
} }
}
return true; return true;
} }
@@ -561,6 +547,30 @@ bool USkillTreeComponent::HasChildrenPurchased(FName SkillID) const
return false; 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) void USkillTreeComponent::ApplySkillEffects(const FSkillNodeData& SkillData)
{ {
// This is where you would apply the actual skill effects to the character // This is where you would apply the actual skill effects to the character

View File

@@ -24,19 +24,18 @@ protected:
// Current skill points available // Current skill points available
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Skill Points", SaveGame) UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Skill Points", SaveGame)
int32 AvailableSkillPoints; int32 AvailableSkillPoints = 0;
// Total skill points earned // Total skill points earned
UPROPERTY(BlueprintReadOnly, Category = "Skill Points", SaveGame) UPROPERTY(BlueprintReadOnly, Category = "Skill Points", SaveGame)
int32 TotalSkillPointsEarned; int32 TotalSkillPointsEarned = 0;
// Data table containing all skill definitions // Data table containing all skill definitions
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Skill Tree") UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Skill Tree")
class UDataTable* SkillDataTable; class UDataTable* SkillDataTable = nullptr;
// All skills loaded from the data table // All skills loaded from the data table
UPROPERTY(BlueprintReadOnly, Category = "Skill Tree") UPROPERTY(BlueprintReadOnly, Category = "Skill Tree")
TMap<FName, FSkillNodeRuntime> AllSkills; TMap<FName, FSkillNodeRuntime> AllSkills = {};
// Currently purchased skills // Currently purchased skills
UPROPERTY(BlueprintReadOnly, Category = "Skill Tree", SaveGame) UPROPERTY(BlueprintReadOnly, Category = "Skill Tree", SaveGame)
@@ -93,10 +92,8 @@ public:
// Skill State Queries // Skill State Queries
UFUNCTION(BlueprintPure, Category = "Skill State") UFUNCTION(BlueprintPure, Category = "Skill State")
bool IsSkillPurchased(FName SkillID) const; bool IsSkillPurchased(FName SkillID) const;
UFUNCTION(BlueprintPure, Category = "Skill State") UFUNCTION(BlueprintPure, Category = "Skill State")
bool IsSkillSelected(FName SkillID) const; bool IsSkillSelected(FName SkillID) const;
UFUNCTION(BlueprintPure, Category = "Skill State") UFUNCTION(BlueprintPure, Category = "Skill State")
bool IsSkillAvailable(FName SkillID) const; bool IsSkillAvailable(FName SkillID) const;
@@ -116,19 +113,15 @@ public:
// Calculate total bonuses from purchased skills // Calculate total bonuses from purchased skills
UFUNCTION(BlueprintPure, Category = "Skill Bonuses") UFUNCTION(BlueprintPure, Category = "Skill Bonuses")
float GetTotalHealthBonus() const; float GetTotalHealthBonus() const;
UFUNCTION(BlueprintPure, Category = "Skill Bonuses") UFUNCTION(BlueprintPure, Category = "Skill Bonuses")
float GetTotalDamageBonus() const; float GetTotalDamageBonus() const;
UFUNCTION(BlueprintPure, Category = "Skill Bonuses") UFUNCTION(BlueprintPure, Category = "Skill Bonuses")
float GetTotalSpeedBonus() const; float GetTotalSpeedBonus() const;
UFUNCTION(BlueprintPure, Category = "Skill Bonuses") UFUNCTION(BlueprintPure, Category = "Skill Bonuses")
float GetTotalHealthBonusPercent() const; float GetTotalHealthBonusPercent() const;
UFUNCTION(BlueprintPure, Category = "Skill Bonuses") UFUNCTION(BlueprintPure, Category = "Skill Bonuses")
float GetTotalDamageBonusPercent() const; float GetTotalDamageBonusPercent() const;
UFUNCTION(BlueprintPure, Category = "Skill Bonuses") UFUNCTION(BlueprintPure, Category = "Skill Bonuses")
float GetTotalSpeedBonusPercent() const; float GetTotalSpeedBonusPercent() const;
@@ -139,16 +132,12 @@ public:
// Events // Events
UPROPERTY(BlueprintAssignable, Category = "Skill Events") UPROPERTY(BlueprintAssignable, Category = "Skill Events")
FOnSkillPointsChanged OnSkillPointsChanged; FOnSkillPointsChanged OnSkillPointsChanged;
UPROPERTY(BlueprintAssignable, Category = "Skill Events") UPROPERTY(BlueprintAssignable, Category = "Skill Events")
FOnSkillPurchased OnSkillPurchased; FOnSkillPurchased OnSkillPurchased;
UPROPERTY(BlueprintAssignable, Category = "Skill Events") UPROPERTY(BlueprintAssignable, Category = "Skill Events")
FOnSkillSelectionChanged OnSkillSelectionChanged; FOnSkillSelectionChanged OnSkillSelectionChanged;
UPROPERTY(BlueprintAssignable, Category = "Skill Events") UPROPERTY(BlueprintAssignable, Category = "Skill Events")
FOnSkillStateChanged OnSkillStateChanged; FOnSkillStateChanged OnSkillStateChanged;
UPROPERTY(BlueprintAssignable, Category = "Skill Events") UPROPERTY(BlueprintAssignable, Category = "Skill Events")
FOnSelectionError OnSelectionError; FOnSelectionError OnSelectionError;
@@ -156,8 +145,9 @@ private:
// Internal helper functions // Internal helper functions
void UpdateSkillStates(); void UpdateSkillStates();
void UpdateSelectionCost(); void UpdateSelectionCost();
bool ArePrerequisitesMet(const FSkillNodeData& SkillData) const; bool ArePrerequisitesMet(const FSkillNodeData& SkillData, bool bIncludeSelected = false) const;
bool HasChildrenPurchased(FName SkillID) const; bool HasChildrenPurchased(FName SkillID) const;
void ApplySkillEffects(const FSkillNodeData& SkillData); void ApplySkillEffects(const FSkillNodeData& SkillData);
void RemoveSkillEffects(const FSkillNodeData& SkillData); void RemoveSkillEffects(const FSkillNodeData& SkillData);
void CascadeDeselectDependents(FName SkillID);
}; };