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" }
}
maxRanksinferred from theranksarray = 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: 1means 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():
- Detection: Scans spell icon CDN URLs for the
"apextalent"marker - 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 - Injection: Adds
apexSubTalents(array) andapexMaxRanks(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
| File | Role |
|---|---|
-TalentNode.tsx | Renders apex nodes with arc pips; applies rank inflation |
-TalentTooltip.tsx | Shows main talent + sub-talent descriptions with investment allocation |
-TalentTreeRenderer.tsx | Builds 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
| Pitfall | Explanation |
|---|---|
Using apexMaxRanks as the investment denominator | Apex talents cost 1 point; apexMaxRanks is a descriptive count, not a talent-point count |
Expecting rank > 1 from the profile API | Blizzard caps at node.maxRanks (1 for apex); sub-talent ranks are implicit |
Expecting sub-talent IDs in selected_spec_talents | Sub-talents share the parent node; they are not separate selection entries |
| CDN URL detection missing new apex talents | GDL 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