Fifty Skill Tree

Interactive skill tree widget for Flutter games — customizable, animated, and game-ready. Part of Fifty Flutter Kit.

Home Basic Tree Node Unlock RPG Skill Tree

Features

  • Multiple Layout Algorithms - Vertical, horizontal, radial, grid, and custom positioning
  • Skill States - Locked, available, unlocked, and maxed states with visual feedback
  • Point/Currency System - Built-in point management for unlocking skills
  • Prerequisites & Dependencies - Automatic prerequisite checking and dependency visualization
  • Unlock Animations - Smooth animations for unlocking, pulsing, and highlighting
  • Save/Load Progress - JSON serialization for game save files
  • Mobile-Friendly - Touch interactions with pan and pinch-to-zoom support
  • Fully Customizable Theming - Dark/light preset themes, FDL integration, and full customization support
  • Generic Data Support - Attach any custom data to skill nodes

Installation

Add to your pubspec.yaml:

dependencies:
  fifty_skill_tree: ^0.1.0

Then run:

flutter pub get

Quick Start

1. Create a Skill Tree

import 'package:fifty_skill_tree/fifty_skill_tree.dart';

// Create a skill tree
final tree = SkillTree<void>(
  id: 'warrior',
  name: 'Warrior Skills',
);

// Add nodes
tree.addNode(SkillNode(
  id: 'slash',
  name: 'Slash',
  description: 'A basic sword attack',
  costs: [1],
  tier: 0,
));

tree.addNode(SkillNode(
  id: 'power_slash',
  name: 'Power Slash',
  description: 'A powerful overhead strike',
  costs: [2],
  prerequisites: ['slash'],
  tier: 1,
));

tree.addNode(SkillNode(
  id: 'whirlwind',
  name: 'Whirlwind',
  description: 'Spin attack hitting all nearby enemies',
  costs: [3],
  prerequisites: ['power_slash'],
  tier: 2,
));

// Add connections for visualization
tree.addConnection(SkillConnection(fromId: 'slash', toId: 'power_slash'));
tree.addConnection(SkillConnection(fromId: 'power_slash', toId: 'whirlwind'));

// Give the player some points
tree.addPoints(10);

2. Create a Controller

final controller = SkillTreeController<void>(
  tree: tree,
  theme: SkillTreeTheme.dark(),
);

// Listen for changes
controller.addListener(() {
  print('Points: ${controller.availablePoints}');
});

3. Display the Tree

SkillTreeView<void>(
  controller: controller,
  layout: const VerticalTreeLayout(),
  onNodeTap: (node) async {
    final result = await controller.unlock(node.id);
    if (result.success) {
      print('Unlocked ${node.name}!');
    } else {
      print('Cannot unlock: ${result.reason}');
    }
  },
)

Architecture

SkillTreeView<T> (Widget)
    |
    +-- SkillTreeController<T>
    |       State management, unlock logic, point tracking
    |
    +-- SkillTree<T>
    |       +-- SkillNode<T> (nodes with conditions, costs, levels)
    |       +-- SkillConnection (prerequisite relationships)
    |
    +-- TreeLayout (Abstract)
    |       +-- VerticalTreeLayout
    |       +-- HorizontalTreeLayout
    |       +-- RadialTreeLayout
    |       +-- GridLayout
    |       +-- CustomLayout
    |
    +-- SkillTreeTheme
            Visual styling (dark/light/custom/FDL defaults)

Core Components

Component Description
SkillTree<T> Container for nodes and connections
SkillNode<T> Individual skill with costs, levels, prerequisites
SkillConnection Defines relationships between nodes
SkillTreeController<T> State management, unlock logic, serialization
SkillTreeView<T> Widget that renders the tree with interactions
SkillTreeTheme Visual styling configuration

API Reference

SkillTree

final tree = SkillTree<void>(
  id: 'warrior',
  name: 'Warrior Skills',
);

tree.addNode(node);
tree.addConnection(connection);
tree.addPoints(10);

SkillNode

Basic Node:

SkillNode(
  id: 'fireball',
  name: 'Fireball',
  description: 'Launches a ball of fire at enemies',
  costs: [1], // 1 point to unlock
)

Node Types:

SkillNode(
  id: 'skill',
  name: 'Skill',
  type: SkillType.passive,   // Passive skill
  // type: SkillType.active,  // Active ability
  // type: SkillType.ultimate, // Ultimate skill
  // type: SkillType.keystone, // Keystone/capstone
)

SkillConnection

Basic Connection:

tree.addConnection(SkillConnection(
  fromId: 'slash',
  toId: 'power_slash',
));

Connection Types:

// Required connection (must unlock parent first)
SkillConnection(
  fromId: 'a',
  toId: 'b',
  type: ConnectionType.required,
)

// Optional connection (can skip parent)
SkillConnection(
  fromId: 'a',
  toId: 'b',
  type: ConnectionType.optional,
)

// Exclusive connection (only one can be unlocked)
SkillConnection(
  fromId: 'a',
  toId: 'b',
  type: ConnectionType.exclusive,
)

SkillTreeController

final controller = SkillTreeController<void>(
  tree: tree,
  theme: SkillTreeTheme.dark(),
);

controller.addListener(() {
  print('Points: ${controller.availablePoints}');
});

SkillTreeView

SkillTreeView<void>(
  controller: controller,
  layout: const VerticalTreeLayout(),
  onNodeTap: (node) {
    controller.unlock(node.id);
  },
  onNodeLongPress: (node) {
    showSkillDetails(context, node);
  },
)

SkillTreeTheme

Built-in Themes:

// Dark theme (default)
final theme = SkillTreeTheme.dark();

// Light theme
final theme = SkillTreeTheme.light();

When no theme is provided, the widgets use FDL (Fifty Design Language) defaults automatically.

Custom Theme:

final customTheme = SkillTreeTheme(
  // Node colors by state
  lockedNodeColor: Colors.grey[800]!,
  lockedNodeBorderColor: Colors.grey[600]!,
  availableNodeColor: Colors.blue[900]!,
  availableNodeBorderColor: Colors.blue[400]!,
  unlockedNodeColor: Colors.green[900]!,
  unlockedNodeBorderColor: Colors.green[400]!,
  maxedNodeColor: Colors.amber[900]!,
  maxedNodeBorderColor: Colors.amber[400]!,

  // Connection colors
  connectionLockedColor: Colors.grey[600]!,
  connectionUnlockedColor: Colors.green[400]!,

  // Sizes
  nodeRadius: 28.0,
  nodeBorderWidth: 2.0,
  connectionWidth: 2.0,
);

Apply Theme:

// At creation
final controller = SkillTreeController(
  tree: tree,
  theme: customTheme,
);

// Or update later
controller.setTheme(SkillTreeTheme.light());

// Or revert to FDL defaults
controller.setTheme(null);

Layouts

Vertical Layout (Default) - Traditional top-to-bottom skill tree layout:

SkillTreeView(
  controller: controller,
  layout: const VerticalTreeLayout(),
)

Horizontal Layout - Left-to-right skill tree layout:

SkillTreeView(
  controller: controller,
  layout: const HorizontalTreeLayout(),
)

Radial Layout - Circular layout with skills radiating from center:

SkillTreeView(
  controller: controller,
  layout: const RadialTreeLayout(
    startAngle: -90, // Start from top
    sweepAngle: 360, // Full circle
  ),
)

Grid Layout - Organize skills in a grid pattern:

SkillTreeView(
  controller: controller,
  layout: const GridLayout(
    columns: 4,
    rows: 3,
  ),
)

Custom Layout - Provide your own positioning logic:

SkillTreeView(
  controller: controller,
  layout: CustomLayout(
    positionBuilder: (node, index, total, size) {
      // Return custom Offset for each node
      return Offset(node.position?.dx ?? 0, node.position?.dy ?? 0);
    },
  ),
)

Enums

Enum Values
SkillState locked, available, unlocked, maxed
SkillType passive, active, ultimate, keystone, minor
ConnectionType required, optional, exclusive
ConnectionStyle solid, dashed, animated

Serialization

Save Progress:

// Export only progress (compact, for save games)
final progress = controller.exportProgress();
final jsonString = jsonEncode(progress);
await saveToFile(jsonString);

// Export full tree (includes structure)
final fullExport = controller.exportTree();

Load Progress:

final jsonString = await loadFromFile();
final progress = jsonDecode(jsonString) as Map<String, dynamic>;
controller.importProgress(progress);

Example Progress JSON:

{
  "availablePoints": 5,
  "nodes": {
    "slash": 1,
    "power_slash": 2,
    "whirlwind": 1
  }
}

Usage Patterns

Node Interactions

SkillTreeView(
  controller: controller,
  onNodeTap: (node) {
    // Handle tap - typically unlock the skill
    controller.unlock(node.id);
  },
  onNodeLongPress: (node) {
    // Handle long press - show details
    showSkillDetails(context, node);
  },
)

View Controls

// Enable/disable interactions
SkillTreeView(
  controller: controller,
  enablePan: true,   // Allow panning
  enableZoom: true,  // Allow zooming
  minZoom: 0.5,      // Minimum zoom level
  maxZoom: 2.0,      // Maximum zoom level
  initialZoom: 1.0,  // Starting zoom
)

Programmatic Control

// Zoom to specific level
controller.zoomTo(1.5);

// Pan to offset
controller.panTo(Offset(100, 50));

// Focus on a specific node
controller.focusNode(
  'fireball',
  nodePositions: calculatedPositions,
  viewSize: viewSize,
);

// Reset view
controller.resetView();

Point Management

// Add points (e.g., on level up)
controller.addPoints(5);

// Remove points
controller.removePoints(2);

// Set points directly
controller.setPoints(10);

// Check available points
print('Available: ${controller.availablePoints}');
print('Spent: ${controller.spentPoints}');

Unlocking Skills

// Attempt to unlock
final result = await controller.unlock('fireball');

if (result.success) {
  // Skill was unlocked
  print('Unlocked ${result.node?.name} (Level ${result.newLevel})');
  print('Points spent: ${result.pointsSpent}');
} else {
  // Unlock failed - check reason
  switch (result.reason) {
    case UnlockFailureReason.nodeNotFound:
      print('Node does not exist');
      break;
    case UnlockFailureReason.alreadyMaxed:
      print('Skill is already at max level');
      break;
    case UnlockFailureReason.prerequisitesNotMet:
      print('Prerequisites not met');
      break;
    case UnlockFailureReason.insufficientPoints:
      print('Not enough points');
      break;
    case UnlockFailureReason.lockedByExclusive:
      print('Locked by exclusive choice');
      break;
  }
}

Reset Functions

// Reset entire tree (refunds all points)
controller.reset();

// Reset single node (refunds its points)
controller.resetNode('fireball');

Custom Node Widget

SkillTreeView(
  controller: controller,
  nodeBuilder: (node, state) {
    return Container(
      width: 60,
      height: 60,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: _getColorForState(state),
        border: Border.all(color: Colors.white, width: 2),
      ),
      child: Center(
        child: Icon(
          node.icon ?? Icons.star,
          color: Colors.white,
        ),
      ),
    );
  },
)

Multi-Level Nodes

SkillNode(
  id: 'fireball',
  name: 'Fireball',
  maxLevel: 5,
  costs: [1, 1, 2, 2, 3], // Cost for each level
)

Node with Custom Data

// Define your data class
class AbilityData {
  final int damage;
  final double cooldown;

  AbilityData({required this.damage, required this.cooldown});
}

// Create typed tree and node
final tree = SkillTree<AbilityData>(id: 'mage', name: 'Mage Skills');

tree.addNode(SkillNode<AbilityData>(
  id: 'fireball',
  name: 'Fireball',
  data: AbilityData(damage: 50, cooldown: 2.0),
));

// Access data later
final node = tree.getNode('fireball');
print('Damage: ${node?.data?.damage}');

Platform Support

Platform Support Notes
Android Yes
iOS Yes
macOS Yes
Linux Yes
Windows Yes
Web Yes

Fifty Design Language Integration

This package is part of Fifty Flutter Kit:

  • FDL defaults - When no theme is provided, widgets use Fifty Design Language defaults automatically
  • Theme presets - Built-in dark/light themes, or full custom theming
  • Token alignment - Compatible with fifty_tokens, fifty_theme, fifty_ui

Version

0.1.0


License

MIT License - see LICENSE for details.

Part of Fifty Flutter Kit.

Libraries

fifty_skill_tree
Interactive skill tree widget for Flutter games.