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;
|
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
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user