Skip to main content

Talent Tree — Apex Talent Data Shape

Apex talents are capstone abilities at the bottom of WoW specialization trees. They have a unique data shape that differs from regular talents and requires special handling in both the GDL sync and frontend rendering.

How Apex Talents Work in WoW

An apex talent costs 1 talent point to unlock. That single point activates the main effect and all sub-effects. There is no separate per-sub-talent investment — the player either has the talent (rank 1) or doesn't (rank 0).

Blizzard API Behavior

Talent Tree API (Game Data)

The talent tree endpoint returns apex nodes with a single rank entry:

{
"id": 110408,
"ranks": [{ "rank": 1, "tooltip": { "talent": { "id": 141755 }, ... } }],
"node_type": { "type": "ACTIVE" }
}
  • maxRanks inferred from the ranks array = 1
  • Sub-talent data is not present in the tree API — it must be enriched separately

Character Profile API

The profile endpoint reports a single entry per apex node:

{
"id": 110408,
"rank": 1
}
  • rank: 1 means the talent is fully invested (the node only accepts 1 point)
  • Sub-talent investments are not reported as separate entries
  • There will never be rank: 2, rank: 3, etc. for an apex node

GDL Enrichment (Rust)

The GDL talent tree sync enriches apex nodes via enrich_apex_talents():

  1. Detection: Scans spell icon CDN URLs for the "apextalent" marker
  2. Sub-talent fetch: For each apex candidate, fetches /data/wow/talent/{id} for the main talent and nearby IDs (talent_id - 10 to talent_id) to find sub-talents by matching spell names
  3. Injection: Adds apexSubTalents (array) and apexMaxRanks (int) to the tooltip

Enriched tooltip shape

{
"talentId": 141755,
"spellName": "Void Apparitions",
"description": "...",
"iconUrl": "...",
"apexMaxRanks": 4,
"apexSubTalents": [
{
"talentId": 141753,
"spellId": 460856,
"spellName": "Void Apparitions",
"rankDescriptions": ["Tentacle Slam has a 100% chance to activate..."],
"iconUrl": null,
"maxRanks": 1
},
{
"talentId": 141754,
"spellId": 461964,
"spellName": "Void Apparitions",
"rankDescriptions": [
"Shadowy Apparitions have a 15% chance to spawn...",
"Shadowy Apparitions have a 30% chance to spawn..."
],
"iconUrl": null,
"maxRanks": 2
}
]
}

apexMaxRanks is informational, not an investment count

apexMaxRanks = 1 (main) + sum(sub.maxRanks). This represents the total number of descriptive tiers across the talent, not how many talent points the node accepts. The node always accepts exactly 1 point.

Frontend Rendering

The rank inflation rule

Because the profile API reports rank: 1 and apexMaxRanks: 4, the frontend must inflate the selected rank for display:

if apex node is selected AND selectedRank >= node.maxRanks (tree max = 1):
effectiveSelectedRank = apexMaxRanks

Without this, the UI would show 1/4 with empty pips — making a fully invested talent look barely invested.

Key files

FileRole
-TalentNode.tsxRenders apex nodes with arc pips; applies rank inflation
-TalentTooltip.tsxShows main talent + sub-talent descriptions with investment allocation
-TalentTreeRenderer.tsxBuilds selection map matching profile rank to tree nodes

Visual elements

  • Larger circle (1.5x node size) with a round border
  • Arc pips around the node — one per sub-talent plus one for the main talent
  • Rank badge only shown when partially invested (hidden when fully invested)
  • Tooltip shows main description + each sub-talent with rank breakdowns

Common Pitfalls

PitfallExplanation
Using apexMaxRanks as the investment denominatorApex talents cost 1 point; apexMaxRanks is a descriptive count, not a talent-point count
Expecting rank > 1 from the profile APIBlizzard caps at node.maxRanks (1 for apex); sub-talent ranks are implicit
Expecting sub-talent IDs in selected_spec_talentsSub-talents share the parent node; they are not separate selection entries
CDN URL detection missing new apex talentsGDL detects apex via "apextalent" in the spell icon CDN URL; new talents may use different URLs

Data Flow Summary

Blizzard Talent Tree API
→ GDL sync: transform_node() → infer_max_ranks() = 1
→ GDL sync: enrich_apex_talents() → adds apexSubTalents + apexMaxRanks
→ Stored in wow_talent_trees (JSONB)

Blizzard Profile API
→ GDL enrichment: store_specializations() → raw JSONB
→ GDL query: transform_talents() → {id: nodeId, rank: 1}

Frontend
→ useGetTalentTreeQuery() → tree with apex enrichment
→ useGetCharacterSpecializationsQuery() → selected talents with rank: 1
→ buildSelectionMap() → maps nodeId → {rank: 1}
→ TalentNodeComponent → detects isApex, inflates rank to apexMaxRanks
→ TalentTooltip → allocates inflated rank across main + sub-talents