feat: initial commit, only bugs remaining

This commit is contained in:
2025-10-11 03:33:42 +02:00
parent 9938181043
commit be8ea7a497
148 changed files with 1898 additions and 162 deletions

View File

@@ -0,0 +1,192 @@
#include "CharacterStatsComponent.h"
#include "SkillTreeComponent.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
UCharacterStatsComponent::UCharacterStatsComponent()
{
PrimaryComponentTick.bCanEverTick = false;
// Default base stats
BaseMaxHealth = 100.0f;
BaseDamage = 10.0f;
BaseSpeed = 600.0f;
// Initialize current values
CurrentHealth = BaseMaxHealth;
MaxHealth = BaseMaxHealth;
CurrentDamage = BaseDamage;
CurrentSpeed = BaseSpeed;
// Initialize bonuses to 0
SkillHealthBonus = 0.0f;
SkillDamageBonus = 0.0f;
SkillSpeedBonus = 0.0f;
SkillHealthBonusPercent = 0.0f;
SkillDamageBonusPercent = 0.0f;
SkillSpeedBonusPercent = 0.0f;
}
void UCharacterStatsComponent::BeginPlay()
{
Super::BeginPlay();
// Find the skill tree component on the same actor
AActor* Owner = GetOwner();
if (Owner)
{
OwnerSkillTree = Owner->FindComponentByClass<USkillTreeComponent>();
// Bind to skill tree events
if (OwnerSkillTree)
{
OwnerSkillTree->OnSkillPurchased.AddDynamic(this, &UCharacterStatsComponent::OnSkillPurchased);
}
}
InitializeStats();
}
void UCharacterStatsComponent::InitializeStats()
{
MaxHealth = BaseMaxHealth;
CurrentHealth = MaxHealth;
CurrentDamage = BaseDamage;
CurrentSpeed = BaseSpeed;
RecalculateStats();
}
void UCharacterStatsComponent::OnSkillPurchased(FName SkillID)
{
// Called when a skill is purchased, update all stats
UpdateStatsFromSkillTree();
}
void UCharacterStatsComponent::UpdateStatsFromSkillTree()
{
if (!OwnerSkillTree)
{
return;
}
// Reset skill bonuses
SkillHealthBonus = 0.0f;
SkillDamageBonus = 0.0f;
SkillSpeedBonus = 0.0f;
SkillHealthBonusPercent = 0.0f;
SkillDamageBonusPercent = 0.0f;
SkillSpeedBonusPercent = 0.0f;
// Calculate total bonuses from purchased skills
TArray<FSkillNodeRuntime> AllSkills = OwnerSkillTree->GetAllSkillNodes();
for (const FSkillNodeRuntime& Skill : AllSkills)
{
if (Skill.CurrentState == ESkillNodeState::Purchased)
{
// Add flat bonuses
SkillHealthBonus += Skill.NodeData.HealthBonus;
SkillDamageBonus += Skill.NodeData.DamageBonus;
SkillSpeedBonus += Skill.NodeData.SpeedBonus;
// Add percentage bonuses
SkillHealthBonusPercent += Skill.NodeData.HealthBonusPercent;
SkillDamageBonusPercent += Skill.NodeData.DamageBonusPercent;
SkillSpeedBonusPercent += Skill.NodeData.SpeedBonusPercent;
}
}
RecalculateStats();
}
void UCharacterStatsComponent::RecalculateStats()
{
// Calculate final stats: Base + Flat Bonus + (Base * Percentage Bonus)
float OldMaxHealth = MaxHealth;
MaxHealth = BaseMaxHealth + SkillHealthBonus + (BaseMaxHealth * SkillHealthBonusPercent / 100.0f);
CurrentDamage = BaseDamage + SkillDamageBonus + (BaseDamage * SkillDamageBonusPercent / 100.0f);
CurrentSpeed = BaseSpeed + SkillSpeedBonus + (BaseSpeed * SkillSpeedBonusPercent / 100.0f);
// Scale current health proportionally if max health changed
if (OldMaxHealth > 0 && MaxHealth != OldMaxHealth)
{
float HealthPercent = CurrentHealth / OldMaxHealth;
CurrentHealth = MaxHealth * HealthPercent;
}
// Apply speed to character movement
ApplySpeedToMovement();
// Broadcast events
OnHealthChanged.Broadcast(CurrentHealth, MaxHealth);
OnDamageChanged.Broadcast(CurrentDamage);
OnSpeedChanged.Broadcast(CurrentSpeed);
OnStatsUpdated.Broadcast();
UE_LOG(LogTemp, Log, TEXT("Stats Updated - Health: %.0f/%.0f, Damage: %.0f, Speed: %.0f"),
CurrentHealth, MaxHealth, CurrentDamage, CurrentSpeed);
}
void UCharacterStatsComponent::TakeDamage(float DamageAmount)
{
if (DamageAmount <= 0.0f)
{
return;
}
CurrentHealth = FMath::Clamp(CurrentHealth - DamageAmount, 0.0f, MaxHealth);
OnHealthChanged.Broadcast(CurrentHealth, MaxHealth);
if (CurrentHealth <= 0.0f)
{
UE_LOG(LogTemp, Warning, TEXT("Character has died!"));
}
}
void UCharacterStatsComponent::Heal(float HealAmount)
{
if (HealAmount <= 0.0f)
{
return;
}
CurrentHealth = FMath::Clamp(CurrentHealth + HealAmount, 0.0f, MaxHealth);
OnHealthChanged.Broadcast(CurrentHealth, MaxHealth);
}
void UCharacterStatsComponent::SetHealth(float NewHealth)
{
CurrentHealth = FMath::Clamp(NewHealth, 0.0f, MaxHealth);
OnHealthChanged.Broadcast(CurrentHealth, MaxHealth);
}
FText UCharacterStatsComponent::GetHealthText() const
{
return FText::Format(FText::FromString(TEXT("{0:.0f} / {1:.0f}")),
FText::AsNumber(CurrentHealth),
FText::AsNumber(MaxHealth));
}
FText UCharacterStatsComponent::GetDamageText() const
{
return FText::Format(FText::FromString(TEXT("Damage: {0:.0f}")),
FText::AsNumber(CurrentDamage));
}
FText UCharacterStatsComponent::GetSpeedText() const
{
return FText::Format(FText::FromString(TEXT("Speed: {0:.0f}")),
FText::AsNumber(CurrentSpeed));
}
void UCharacterStatsComponent::ApplySpeedToMovement()
{
// Apply speed to character movement component
ACharacter* CharacterOwner = Cast<ACharacter>(GetOwner());
if (CharacterOwner && CharacterOwner->GetCharacterMovement())
{
CharacterOwner->GetCharacterMovement()->MaxWalkSpeed = CurrentSpeed;
UE_LOG(LogTemp, Log, TEXT("Applied movement speed: %.0f"), CurrentSpeed);
}
}

View File

@@ -0,0 +1,140 @@
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CharacterStatsComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnHealthChanged, float, CurrentHealth, float, MaxHealth);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDamageChanged, float, CurrentDamage);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSpeedChanged, float, CurrentSpeed);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnStatsUpdated);
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class UTAD_UI_API UCharacterStatsComponent : public UActorComponent
{
GENERATED_BODY()
public:
UCharacterStatsComponent();
protected:
virtual void BeginPlay() override;
// Base stats (without any bonuses)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Base Stats")
float BaseMaxHealth;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Base Stats")
float BaseDamage;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Base Stats")
float BaseSpeed;
// Current values
UPROPERTY(BlueprintReadOnly, Category = "Current Stats")
float CurrentHealth;
// Calculated stats (base + bonuses)
UPROPERTY(BlueprintReadOnly, Category = "Current Stats")
float MaxHealth;
UPROPERTY(BlueprintReadOnly, Category = "Current Stats")
float CurrentDamage;
UPROPERTY(BlueprintReadOnly, Category = "Current Stats")
float CurrentSpeed;
// Skill tree bonuses (flat bonuses)
UPROPERTY(BlueprintReadOnly, Category = "Skill Bonuses")
float SkillHealthBonus;
UPROPERTY(BlueprintReadOnly, Category = "Skill Bonuses")
float SkillDamageBonus;
UPROPERTY(BlueprintReadOnly, Category = "Skill Bonuses")
float SkillSpeedBonus;
// Skill tree bonuses (percentage bonuses)
UPROPERTY(BlueprintReadOnly, Category = "Skill Bonuses")
float SkillHealthBonusPercent;
UPROPERTY(BlueprintReadOnly, Category = "Skill Bonuses")
float SkillDamageBonusPercent;
UPROPERTY(BlueprintReadOnly, Category = "Skill Bonuses")
float SkillSpeedBonusPercent;
public:
// Initialize stats
UFUNCTION(BlueprintCallable, Category = "Character Stats")
void InitializeStats();
// Update stats from skill tree
UFUNCTION(BlueprintCallable, Category = "Character Stats")
void UpdateStatsFromSkillTree();
// Called when a skill is purchased
UFUNCTION()
void OnSkillPurchased(FName SkillID);
// Calculate final stats
UFUNCTION(BlueprintCallable, Category = "Character Stats")
void RecalculateStats();
// Getters for current stats
UFUNCTION(BlueprintPure, Category = "Character Stats")
float GetCurrentHealth() const { return CurrentHealth; }
UFUNCTION(BlueprintPure, Category = "Character Stats")
float GetMaxHealth() const { return MaxHealth; }
UFUNCTION(BlueprintPure, Category = "Character Stats")
float GetHealthPercentage() const { return MaxHealth > 0 ? CurrentHealth / MaxHealth : 0.0f; }
UFUNCTION(BlueprintPure, Category = "Character Stats")
float GetCurrentDamage() const { return CurrentDamage; }
UFUNCTION(BlueprintPure, Category = "Character Stats")
float GetCurrentSpeed() const { return CurrentSpeed; }
// Health management
UFUNCTION(BlueprintCallable, Category = "Character Stats")
void TakeDamage(float DamageAmount);
UFUNCTION(BlueprintCallable, Category = "Character Stats")
void Heal(float HealAmount);
UFUNCTION(BlueprintCallable, Category = "Character Stats")
void SetHealth(float NewHealth);
// Get formatted text for UI
UFUNCTION(BlueprintPure, Category = "Character Stats UI")
FText GetHealthText() const;
UFUNCTION(BlueprintPure, Category = "Character Stats UI")
FText GetDamageText() const;
UFUNCTION(BlueprintPure, Category = "Character Stats UI")
FText GetSpeedText() const;
// Events
UPROPERTY(BlueprintAssignable, Category = "Stats Events")
FOnHealthChanged OnHealthChanged;
UPROPERTY(BlueprintAssignable, Category = "Stats Events")
FOnDamageChanged OnDamageChanged;
UPROPERTY(BlueprintAssignable, Category = "Stats Events")
FOnSpeedChanged OnSpeedChanged;
UPROPERTY(BlueprintAssignable, Category = "Stats Events")
FOnStatsUpdated OnStatsUpdated;
private:
// Apply speed to character movement
void ApplySpeedToMovement();
// Reference to owner's skill tree component
UPROPERTY()
class USkillTreeComponent* OwnerSkillTree;
};

View File

@@ -0,0 +1,249 @@
#include "SkillTreeBlueprintLibrary.h"
#include "SkillTreeComponent.h"
FLinearColor USkillTreeBlueprintLibrary::GetSkillStateColor(ESkillNodeState State)
{
switch (State)
{
case ESkillNodeState::Locked:
return FLinearColor(0.3f, 0.3f, 0.3f, 1.0f); // Gray
case ESkillNodeState::Available:
return FLinearColor(1.0f, 1.0f, 1.0f, 1.0f); // White
case ESkillNodeState::Selected:
return FLinearColor(1.0f, 0.8f, 0.0f, 1.0f); // Gold
case ESkillNodeState::Purchased:
return FLinearColor(0.0f, 1.0f, 0.0f, 1.0f); // Green
default:
return FLinearColor::White;
}
}
FText USkillTreeBlueprintLibrary::GetSkillStateText(ESkillNodeState State)
{
switch (State)
{
case ESkillNodeState::Locked:
return FText::FromString(TEXT("Locked"));
case ESkillNodeState::Available:
return FText::FromString(TEXT("Available"));
case ESkillNodeState::Selected:
return FText::FromString(TEXT("Selected"));
case ESkillNodeState::Purchased:
return FText::FromString(TEXT("Purchased"));
default:
return FText::FromString(TEXT("Unknown"));
}
}
FText USkillTreeBlueprintLibrary::GetSkillAvailabilityText(USkillTreeComponent* SkillTreeComponent, FName SkillID)
{
if (!SkillTreeComponent)
{
return FText::FromString(TEXT("No Skill Tree Component"));
}
FSkillNodeRuntime NodeData;
if (!SkillTreeComponent->GetSkillNodeData(SkillID, NodeData))
{
return FText::FromString(TEXT("Invalid Skill"));
}
ESkillNodeState State = SkillTreeComponent->GetSkillState(SkillID);
switch (State)
{
case ESkillNodeState::Purchased:
return FText::FromString(TEXT("Already Purchased"));
case ESkillNodeState::Selected:
return FText::FromString(TEXT("Selected for Purchase"));
case ESkillNodeState::Available:
return FText::Format(FText::FromString(TEXT("Available - Cost: {0} Points")), NodeData.NodeData.Cost);
case ESkillNodeState::Locked:
{
// Check why it's locked
if (NodeData.NodeData.Prerequisites.Num() > 0)
{
FString MissingPrereqs;
for (const FName& PrereqID : NodeData.NodeData.Prerequisites)
{
if (!SkillTreeComponent->IsSkillPurchased(PrereqID))
{
FSkillNodeRuntime PrereqData;
if (SkillTreeComponent->GetSkillNodeData(PrereqID, PrereqData))
{
if (!MissingPrereqs.IsEmpty())
{
MissingPrereqs += TEXT(", ");
}
MissingPrereqs += PrereqData.NodeData.DisplayName.ToString();
}
}
}
return FText::Format(FText::FromString(TEXT("Requires: {0}")), FText::FromString(MissingPrereqs));
}
return FText::FromString(TEXT("Locked"));
}
default:
return FText::FromString(TEXT("Unknown State"));
}
}
FText USkillTreeBlueprintLibrary::FormatSkillDescription(const FSkillNodeData& NodeData)
{
FString Description = NodeData.Description.ToString();
// Add effect descriptions
FString Effects;
if (NodeData.HealthBonus > 0)
{
Effects += FString::Printf(TEXT("\n+%.0f Health"), NodeData.HealthBonus);
}
if (NodeData.HealthBonusPercent > 0)
{
Effects += FString::Printf(TEXT("\n+%.0f%% Health"), NodeData.HealthBonusPercent);
}
if (NodeData.DamageBonus > 0)
{
Effects += FString::Printf(TEXT("\n+%.0f Damage"), NodeData.DamageBonus);
}
if (NodeData.DamageBonusPercent > 0)
{
Effects += FString::Printf(TEXT("\n+%.0f%% Damage"), NodeData.DamageBonusPercent);
}
if (NodeData.SpeedBonus > 0)
{
Effects += FString::Printf(TEXT("\n+%.0f Speed"), NodeData.SpeedBonus);
}
if (NodeData.SpeedBonusPercent > 0)
{
Effects += FString::Printf(TEXT("\n+%.0f%% Speed"), NodeData.SpeedBonusPercent);
}
if (!Effects.IsEmpty())
{
Description += TEXT("\n\nEffects:") + Effects;
}
return FText::FromString(Description);
}
float USkillTreeBlueprintLibrary::GetPreviewTotalDamageBonus(USkillTreeComponent* SkillTreeComponent)
{
if (!SkillTreeComponent)
{
return 0.0f;
}
float Total = SkillTreeComponent->GetTotalDamageBonus();
// Add selected (but not purchased) skills
TArray<FName> SelectedSkills = SkillTreeComponent->GetSelectedSkills();
for (const FName& SkillID : SelectedSkills)
{
FSkillNodeRuntime NodeData;
if (SkillTreeComponent->GetSkillNodeData(SkillID, NodeData))
{
Total += NodeData.NodeData.DamageBonus;
}
}
return Total;
}
float USkillTreeBlueprintLibrary::GetPreviewTotalSpeedBonus(USkillTreeComponent* SkillTreeComponent)
{
if (!SkillTreeComponent)
{
return 0.0f;
}
float Total = SkillTreeComponent->GetTotalSpeedBonus();
// Add selected (but not purchased) skills
TArray<FName> SelectedSkills = SkillTreeComponent->GetSelectedSkills();
for (const FName& SkillID : SelectedSkills)
{
FSkillNodeRuntime NodeData;
if (SkillTreeComponent->GetSkillNodeData(SkillID, NodeData))
{
Total += NodeData.NodeData.SpeedBonus;
}
}
return Total;
}
float USkillTreeBlueprintLibrary::GetPreviewTotalHealthBonus(USkillTreeComponent* SkillTreeComponent)
{
if (!SkillTreeComponent)
{
return 0.0f;
}
float Total = SkillTreeComponent->GetTotalHealthBonus();
// Add selected (but not purchased) skills
TArray<FName> SelectedSkills = SkillTreeComponent->GetSelectedSkills();
for (const FName& SkillID : SelectedSkills)
{
FSkillNodeRuntime NodeData;
if (SkillTreeComponent->GetSkillNodeData(SkillID, NodeData))
{
Total += NodeData.NodeData.HealthBonus;
}
}
return Total;
}
bool USkillTreeBlueprintLibrary::HasSelectedSkills(USkillTreeComponent* SkillTreeComponent)
{
if (!SkillTreeComponent)
{
return false;
}
return SkillTreeComponent->GetSelectedSkills().Num() > 0;
}
bool USkillTreeBlueprintLibrary::CanPlayerRespec(USkillTreeComponent* SkillTreeComponent)
{
if (!SkillTreeComponent)
{
return false;
}
// Check if player has any purchased skills to reset
TArray<FSkillNodeRuntime> AllNodes = SkillTreeComponent->GetAllSkillNodes();
for (const FSkillNodeRuntime& Node : AllNodes)
{
if (Node.CurrentState == ESkillNodeState::Purchased)
{
return true;
}
}
return false;
}
TArray<FName> USkillTreeBlueprintLibrary::GetSkillConnections(USkillTreeComponent* SkillTreeComponent, FName SkillID)
{
TArray<FName> Connections;
if (!SkillTreeComponent)
{
return Connections;
}
FSkillNodeRuntime NodeData;
if (SkillTreeComponent->GetSkillNodeData(SkillID, NodeData))
{
// Return the prerequisites as connections
Connections = NodeData.NodeData.Prerequisites;
}
return Connections;
}

View File

@@ -0,0 +1,56 @@
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "SkillTreeTypes.h"
#include "SkillTreeBlueprintLibrary.generated.h"
// Forward declarations
class USkillTreeComponent;
UCLASS()
class UTAD_UI_API USkillTreeBlueprintLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
// UI Helper Functions
// Get state color for UI display
UFUNCTION(BlueprintPure, Category = "Skill Tree UI", meta = (DisplayName = "Get Skill State Color"))
static FLinearColor GetSkillStateColor(ESkillNodeState State);
// Get state text for UI display
UFUNCTION(BlueprintPure, Category = "Skill Tree UI", meta = (DisplayName = "Get Skill State Text"))
static FText GetSkillStateText(ESkillNodeState State);
// Check if a skill can be selected with detailed reason
UFUNCTION(BlueprintPure, Category = "Skill Tree UI", meta = (DisplayName = "Get Skill Availability Text"))
static FText GetSkillAvailabilityText(USkillTreeComponent* SkillTreeComponent, FName SkillID);
// Get formatted description with current values
UFUNCTION(BlueprintPure, Category = "Skill Tree UI", meta = (DisplayName = "Format Skill Description"))
static FText FormatSkillDescription(const FSkillNodeData& NodeData);
// Calculate total stats preview (including selected but not purchased skills)
UFUNCTION(BlueprintPure, Category = "Skill Tree UI", meta = (DisplayName = "Get Preview Total Health Bonus"))
static float GetPreviewTotalHealthBonus(USkillTreeComponent* SkillTreeComponent);
UFUNCTION(BlueprintPure, Category = "Skill Tree UI", meta = (DisplayName = "Get Preview Total Damage Bonus"))
static float GetPreviewTotalDamageBonus(USkillTreeComponent* SkillTreeComponent);
UFUNCTION(BlueprintPure, Category = "Skill Tree UI", meta = (DisplayName = "Get Preview Total Speed Bonus"))
static float GetPreviewTotalSpeedBonus(USkillTreeComponent* SkillTreeComponent);
// Check if any skills are selected
UFUNCTION(BlueprintPure, Category = "Skill Tree UI", meta = (DisplayName = "Has Selected Skills"))
static bool HasSelectedSkills(USkillTreeComponent* SkillTreeComponent);
// Check if player can respec
UFUNCTION(BlueprintPure, Category = "Skill Tree UI", meta = (DisplayName = "Can Player Respec"))
static bool CanPlayerRespec(USkillTreeComponent* SkillTreeComponent);
// Get the connections between skills for line rendering
UFUNCTION(BlueprintPure, Category = "Skill Tree UI", meta = (DisplayName = "Get Skill Connections"))
static TArray<FName> GetSkillConnections(USkillTreeComponent* SkillTreeComponent, FName SkillID);
};

View File

@@ -0,0 +1,557 @@
#include "SkillTreeComponent.h"
#include "Engine/DataTable.h"
USkillTreeComponent::USkillTreeComponent()
{
PrimaryComponentTick.bCanEverTick = false;
AvailableSkillPoints = 5; // Start with 5 skill points for testing
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;
RuntimeNode.bIsSelected = false;
// 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);
// Check if already purchased
if (SkillNode->CurrentState == ESkillNodeState::Purchased)
{
ErrorMessage = FText::FromString(TEXT("Skill already purchased"));
OnSelectionError.Broadcast(SkillID, ErrorMessage);
return false;
}
// Check if already selected
if (SkillNode->bIsSelected)
{
ErrorMessage = FText::FromString(TEXT("Skill already selected"));
OnSelectionError.Broadcast(SkillID, ErrorMessage);
return false;
}
// For locked skills, we allow selection but will validate on purchase
// This allows players to "plan" their build
if (SkillNode->CurrentState == ESkillNodeState::Locked)
{
// Check if prerequisites would be met with current selection
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;
}
}
// Add to selection
SkillNode->bIsSelected = true;
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->bIsSelected)
{
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
SkillNode->bIsSelected = false;
SelectedSkills.Remove(SkillID);
UpdateSelectionCost();
OnSkillSelectionChanged.Broadcast(CurrentSelectionCost);
OnSkillStateChanged.Broadcast(SkillID);
return true;
}
void USkillTreeComponent::ClearSelection()
{
for (const FName& SkillID : SelectedSkills)
{
if (FSkillNodeRuntime* SkillNode = AllSkills.Find(SkillID))
{
SkillNode->bIsSelected = false;
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;
SkillNode->bIsSelected = false;
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;
}
if (SkillNode->CurrentState == ESkillNodeState::Purchased)
{
OutErrorMessage = FText::FromString(TEXT("Skill already purchased"));
return false;
}
if (SkillNode->bIsSelected)
{
OutErrorMessage = FText::FromString(TEXT("Skill already selected"));
return false;
}
OutErrorMessage = FText::GetEmpty();
return true;
}
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;
Pair.Value.bIsSelected = false;
}
// 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;
// Skip if already purchased
if (SkillNode.CurrentState == ESkillNodeState::Purchased)
{
continue;
}
// Check if prerequisites are met
if (ArePrerequisitesMet(SkillNode.NodeData))
{
SkillNode.CurrentState = SkillNode.bIsSelected ?
ESkillNodeState::Selected : ESkillNodeState::Available;
}
else
{
SkillNode.CurrentState = SkillNode.bIsSelected ?
ESkillNodeState::Selected : ESkillNodeState::Locked;
}
}
}
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());
}

View File

@@ -0,0 +1,163 @@
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "SkillTreeTypes.h"
#include "SkillTreeComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSkillPointsChanged, int32, NewSkillPoints);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSkillPurchased, FName, SkillID);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSkillSelectionChanged, int32, TotalCost);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSkillStateChanged, FName, SkillID);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSelectionError, FName, SkillID, FText, ErrorMessage);
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class UTAD_UI_API USkillTreeComponent : public UActorComponent
{
GENERATED_BODY()
public:
USkillTreeComponent();
protected:
virtual void BeginPlay() override;
// Current skill points available
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Skill Points", SaveGame)
int32 AvailableSkillPoints;
// Total skill points earned
UPROPERTY(BlueprintReadOnly, Category = "Skill Points", SaveGame)
int32 TotalSkillPointsEarned;
// Data table containing all skill definitions
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Skill Tree")
class UDataTable* SkillDataTable;
// All skills loaded from the data table
UPROPERTY(BlueprintReadOnly, Category = "Skill Tree")
TMap<FName, FSkillNodeRuntime> AllSkills;
// Currently purchased skills
UPROPERTY(BlueprintReadOnly, Category = "Skill Tree", SaveGame)
TArray<FName> PurchasedSkills;
// Currently selected skills for purchase
UPROPERTY(BlueprintReadOnly, Category = "Skill Tree")
TArray<FName> SelectedSkills;
// Cache the total cost of selected skills
UPROPERTY(BlueprintReadOnly, Category = "Skill Tree")
int32 CurrentSelectionCost;
public:
// Initialize the skill tree from data table
UFUNCTION(BlueprintCallable, Category = "Skill Tree")
void InitializeSkillTree();
// Skill Points Management
UFUNCTION(BlueprintCallable, Category = "Skill Points")
void AddSkillPoints(int32 Amount);
UFUNCTION(BlueprintCallable, Category = "Skill Points")
void RemoveSkillPoints(int32 Amount);
UFUNCTION(BlueprintPure, Category = "Skill Points")
int32 GetAvailableSkillPoints() const { return AvailableSkillPoints; }
UFUNCTION(BlueprintPure, Category = "Skill Points")
int32 GetTotalSkillPointsEarned() const { return TotalSkillPointsEarned; }
// Selection Management
UFUNCTION(BlueprintCallable, Category = "Skill Selection")
bool SelectSkill(FName SkillID);
UFUNCTION(BlueprintCallable, Category = "Skill Selection")
bool DeselectSkill(FName SkillID);
UFUNCTION(BlueprintCallable, Category = "Skill Selection")
void ClearSelection();
UFUNCTION(BlueprintCallable, Category = "Skill Selection")
bool ConfirmPurchase();
UFUNCTION(BlueprintPure, Category = "Skill Selection")
TArray<FName> GetSelectedSkills() const { return SelectedSkills; }
UFUNCTION(BlueprintPure, Category = "Skill Selection")
int32 GetSelectionCost() const { return CurrentSelectionCost; }
UFUNCTION(BlueprintPure, Category = "Skill Selection")
bool CanAffordSelection() const { return AvailableSkillPoints >= CurrentSelectionCost; }
// 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;
UFUNCTION(BlueprintPure, Category = "Skill State")
bool CanSelectSkill(FName SkillID, FText& OutErrorMessage) const;
UFUNCTION(BlueprintPure, Category = "Skill State")
ESkillNodeState GetSkillState(FName SkillID) const;
// Get skill data
UFUNCTION(BlueprintPure, Category = "Skill Data")
bool GetSkillNodeData(FName SkillID, FSkillNodeRuntime& OutNodeData) const;
UFUNCTION(BlueprintPure, Category = "Skill Data")
TArray<FSkillNodeRuntime> GetAllSkillNodes() const;
// 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;
// Reset skills (for respec feature)
UFUNCTION(BlueprintCallable, Category = "Skill Tree")
void ResetAllSkills();
// 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;
private:
// Internal helper functions
void UpdateSkillStates();
void UpdateSelectionCost();
bool ArePrerequisitesMet(const FSkillNodeData& SkillData) const;
bool HasChildrenPurchased(FName SkillID) const;
void ApplySkillEffects(const FSkillNodeData& SkillData);
void RemoveSkillEffects(const FSkillNodeData& SkillData);
};

View File

@@ -0,0 +1,104 @@
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "SkillTreeTypes.generated.h"
// Enum for skill node state
UENUM(BlueprintType)
enum class ESkillNodeState : uint8
{
Locked UMETA(DisplayName = "Locked"), // Can't be purchased yet (missing prerequisites)
Available UMETA(DisplayName = "Available"), // Can be purchased
Selected UMETA(DisplayName = "Selected"), // Currently selected for purchase
Purchased UMETA(DisplayName = "Purchased") // Already owned
};
// Struct for individual skill node data
// Note: RowName in the DataTable serves as the unique SkillID
USTRUCT(BlueprintType)
struct FSkillNodeData : public FTableRowBase
{
GENERATED_BODY()
// Display name for UI
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill")
FText DisplayName;
// Description of what the skill does
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill")
FText Description;
// Icon for the skill
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill")
class UTexture2D* Icon;
// Skill point cost
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill")
int32 Cost;
// Purchase order priority (lower = buy first, used when confirming multiple skills)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill")
int32 PurchaseOrder;
// Prerequisites - must have these skills first (RowNames of required skills)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Skill")
TArray<FName> Prerequisites;
// Effects this skill provides (actual stat bonuses)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
float HealthBonus;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
float DamageBonus;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
float SpeedBonus;
// Percentage bonuses (multiplicative)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
float HealthBonusPercent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
float DamageBonusPercent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Effects")
float SpeedBonusPercent;
FSkillNodeData()
{
DisplayName = FText::GetEmpty();
Description = FText::GetEmpty();
Icon = nullptr;
Cost = 1;
PurchaseOrder = 0;
HealthBonus = 0.0f;
DamageBonus = 0.0f;
SpeedBonus = 0.0f;
HealthBonusPercent = 0.0f;
DamageBonusPercent = 0.0f;
SpeedBonusPercent = 0.0f;
}
};
// Runtime struct for tracking skill state
USTRUCT(BlueprintType)
struct FSkillNodeRuntime
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Skill")
FSkillNodeData NodeData;
UPROPERTY(BlueprintReadOnly, Category = "Skill")
ESkillNodeState CurrentState;
UPROPERTY(BlueprintReadOnly, Category = "Skill")
bool bIsSelected;
FSkillNodeRuntime()
{
CurrentState = ESkillNodeState::Locked;
bIsSelected = false;
}
};