ServicesTalent Trees

Talent Trees

Server-authoritative progression trees: nodes with per-rank costs, AND/OR prerequisites, tier gates, and mutually exclusive branches; XP curves that convert experience events into spendable points; full and partial respecs with refunds; versioned definitions with declarative player migration; and build analytics. Every example on this page was executed against a live server.

When to reach for it: RPG skill trees with exclusive specializations, learning platforms where exams earn points that unlock electives, SaaS power-user feature ladders driven by usage XP, certification tracks with prerequisites.

When not to: customer points with a redeemable reward catalog and no unlock graph belong to Loyalty; one-time unlock badges belong to Achievements; step-by-step objectives with completion rewards belong to Quest.

Concepts

  • A tree is data - each node has a tier, max_rank, multi-currency cost_per_rank, optional all_of/any_of prerequisites, an optional exclusive_group, and optional per-rank grants.
  • Tier gates hold a tier closed until points spent in the tree and/or required nodes are met; exclusive groups lock out sibling branches until a respec.
  • Points live in one per-user wallet, keyed by free-form currency strings; several trees can spend from the same currency.
  • XP curves turn experience into points. XP accumulates per curve; each level-up credits points_per_level plus any bonus_levels extras. Kinds: linear (level N at per_level * N XP), polynomial (base * N^exponent), table (explicit thresholds).
  • Allocation is server-authoritative - one atomic script checks rank cap, prerequisites, tier gate, exclusive group, and balance, each with its own 409 code, so a modified client cannot cheat.
  • Definitions are versioned - publishing requires a migration plan and players migrate lazily.

Authoring (admin)

snug talent definitions validate --file ./tree.json   # dry-run a draft
snug talent definitions create --file ./tree.json
snug talent curves create --file ./curve.json

Creating, validating, publishing, and deleting definitions and curves needs a platform admin token; any other caller gets 403 insufficient_permissions (verified). Reading does not - any authenticated caller can get and list them. The document shapes, the validator, and how to publish a new version without breaking existing builds are on Authoring and versioning.

Earning points

snug talent xp grant --curve-id docs4-curve --amount 350 --idempotency-key grant-1
snug talent wallet

The examples use a linear curve with per_level: 100, 2 points per level, and a +1 bonus at level 3 (document on the authoring page). 350 XP lands at level 3 and credits 7 points; captured:

{ "idempotent": false, "old_level": 0, "new_level": 3, "granted": { "docs4-points": 7 } }

Replaying the same idempotency_key is a no-op flagged "idempotent": true; the replay does not echo the original outcome (granted empty, levels 0).

Spending points

snug talent state --tree-id docs4-tree     # annotated view of what you can take
snug talent allocate --tree-id docs4-tree --node-id node_ignite --ranks 2

Over HTTP, allocation returns the updated state in the standard envelope; captured live after taking Ignite twice, Firewall, then the tier-2 Inferno:

curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"node_id": "node_inferno"}' \
  http://localhost:4000/api/v1/talent/docs4-tree/allocate
{
  "status": 200,
  "msg": "OK",
  "data": {
    "user_id": "docs4-talent", "tree_id": "docs4-tree", "tree_version": 1,
    "ranks": { "node_ignite": 2, "node_firewall": 1, "node_inferno": 1 },
    "points_spent": { "docs4-points": 5 },
    "exclusive_selections": { "spec": "node_inferno" },
    "highest_tier_unlocked": 1
  }
}

state annotates every node with what a picker UI needs; captured after the allocations above, with 2 points still in the wallet:

{ "node_id": "node_frostfire", "current_rank": 0, "max_rank": 1, "affordable": true,
  "prerequisites_met": true, "tier_locked": false, "excluded_by": "spec" }

The annotations are independent: affordable reports only whether the wallet covers the next rank, so a node can be affordable and still unreachable because tier_locked or excluded_by is set. Ignore highest_tier_unlocked - the server writes a constant 1 and never updates it. Reading another user's state (GET /api/v1/talent/{tree_id}/users/{user_id}/state) is self-or-admin; anyone else gets 403 insufficient_permissions ("cannot act on another user's progression", verified with a second token).

Allocation rules

Every rule violation is a 409 with its own error code; msg spells out the specifics. Each was reproduced live:

error codeReproduced msg tail
insufficient_pointsneed 1 docs4-points, have 0
prereq_unmetnode node_firewall requires 2 rank(s) of node_ignite, have 0
tier_lockednode node_inferno is tier 2, which is not yet unlocked
exclusive_group_violatedgroup spec already has a selection
rank_exceedednode node_ignite has max_rank 3, requested rank 4

An unknown node is a 404 node_not_found instead.

allocate and xp grant both take an idempotency_key, and both draw on one key set per user - not one per tree, not one per operation. Reusing a key any earlier call consumed makes the new call a silent no-op: verified by sending an allocation with the key an earlier xp grant had used, which returned 200 with wallet, ranks, and counters unchanged. allocate carries no idempotent flag, so a swallowed call looks like a successful one. Mint a fresh key per operation.

Respec

snug talent respec --tree-id docs4-tree --node-id node_firewall --ranks 1  # partial
snug talent respec --tree-id docs4-tree --full                             # everything

Refunds follow the policy's refund_ratio (at 1.0 a full respec restored the entire spent balance, verified). Two more 409 codes, both reproduced: respec_dependency when removing ranks another allocated node still requires (node node_firewall depends on node node_ignite), and respec_not_allowed when the tree's policy omits or disables that respec kind (full respec is not enabled for this tree). A full-respec policy can also declare a treasury_fee, charged through the Treasury service.

Analytics (admin)

snug talent analytics paths --tree-id docs4-tree       # per-node take rates
snug talent analytics archetypes --tree-id docs4-tree  # build population shares

Two caveats, both verified. paths counts total_ranks_allocated cumulatively and never decrements - a node allocated, respecced away, and allocated again counted twice - so it measures attraction, not currently held ranks. And both reports are stored per tree version, so publishing restarts them from zero: counters standing at 6 under version 1 read 0 the moment version 2 went live. archetypes returns one row per build, as {"signature": "spec=node_inferno", "population": 1, "share": 1.0} plus the selections behind it; players holding no exclusive selection group under no_capstone.

Events

Allocations, respecs, and migrations land on the caller's Timeline stream talent_{user_id}, tagged ["talent", "{tree_id}"], and on the PubSub channel talent.{user_id}.{tree_id} (both captured live). Each carries an event of talent.allocated, talent.respec, or talent.migrated plus a data object of user_id, tree_id, tree_version, and a per-kind detail - for an allocation, node_id, ranks, and ranks_total.

Limits and configuration

Trees are capped at 1000 nodes, 20 tiers, and 8 currencies; a single allocation at 100 ranks and a single XP event at 10,000,000 XP - all tunable via the TALENT_* variables in the Talent CONFIG reference.

Reference

On this page