Skip to main content

wowlab_types/types/data/
trait.rs

1use serde::{Deserialize, Serialize};
2use wowlab_engine_macros::CopyInsert;
3
4/// Raw `TraitTree` family index retained with serialized talent-tree data.
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6#[repr(i32)]
7#[non_exhaustive]
8pub enum TraitTreeIndex {
9    Class = 1,
10    Specialization = 2,
11    Hero = 3,
12    Selection = 4,
13    Expansion = 6,
14}
15
16impl TraitTreeIndex {
17    #[must_use]
18    pub const fn from_dbc(raw: i32) -> Option<Self> {
19        match raw {
20            1 => Some(Self::Class),
21            2 => Some(Self::Specialization),
22            3 => Some(Self::Hero),
23            4 => Some(Self::Selection),
24            6 => Some(Self::Expansion),
25            _ => None,
26        }
27    }
28}
29
30/// Raw `TraitNode.Type` identity retained with serialized talent-tree data.
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32#[repr(i32)]
33#[non_exhaustive]
34pub enum TraitNodeType {
35    Tiered = 1,
36    Selection = 2,
37    SubtreeSelection = 3,
38}
39
40impl TraitNodeType {
41    #[must_use]
42    pub const fn from_dbc(raw: i32) -> Option<Self> {
43        match raw {
44            1 => Some(Self::Tiered),
45            2 => Some(Self::Selection),
46            3 => Some(Self::SubtreeSelection),
47            _ => None,
48        }
49    }
50}
51
52// docref:start talent-trees-tree-flat
53#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
54#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
55pub struct TraitTreeFlat {
56    pub spec_id: i32,
57    pub spec_name: String,
58    pub class_name: String,
59    pub tree_id: i32,
60    pub all_node_ids: Vec<i32>,
61    #[copy(json)]
62    pub nodes: Vec<TraitNode>,
63    #[copy(json)]
64    pub edges: Vec<TraitEdge>,
65    #[copy(json)]
66    pub sub_trees: Vec<TraitSubTree>,
67    #[copy(json)]
68    pub point_limits: PointLimits,
69}
70/// Expansion-owned trait tree shared by every class and specialization.
71#[derive(Clone, CopyInsert, Debug, Default, Deserialize, Serialize)]
72#[doc = "Expansion trees retain their own identity and selection encoding while reusing the DBC node vocabulary shared with class, specialization, and hero trees."]
73#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
74pub struct ExpansionTraitTreeFlat {
75    pub expansion_id: i32,
76    pub system: String,
77    pub tree_id: i32,
78    pub all_node_ids: Vec<i32>,
79    #[copy(json)]
80    pub nodes: Vec<TraitNode>,
81    #[copy(json)]
82    pub edges: Vec<TraitEdge>,
83    pub max_points: i32,
84}
85// docref:end talent-trees-tree-flat
86
87#[derive(Clone, Debug, Default, Deserialize, Serialize)]
88#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
89pub struct TraitNode {
90    pub id: i32,
91    pub pos_x: i32,
92    pub pos_y: i32,
93    pub max_ranks: i32,
94    #[serde(default)]
95    pub granted_ranks: i32,
96    #[serde(rename = "type")]
97    pub node_type: i32,
98    pub tree_index: i32,
99    pub order_index: i32,
100    pub sub_tree_id: i32,
101    pub entries: Vec<TraitNodeEntry>,
102}
103
104// docref:start talent-trees-node-entry
105#[derive(Clone, Debug, Default, Deserialize, Serialize)]
106#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
107pub struct TraitNodeEntry {
108    pub id: i32,
109    pub definition_id: i32,
110    pub spell_id: i32,
111    /// `TraitDefinition.VisibleSpellID`: the spell `spell_id` is displayed and activated as in-game.
112    #[serde(default)]
113    pub override_spell_id: i32,
114    /// `TraitDefinition.OverridesSpellID`: the learned talent replaces this spell on the action bar.
115    #[serde(default, skip_serializing_if = "is_zero")]
116    pub replaces_spell_id: i32,
117    #[serde(default)]
118    pub sub_tree_id: i32,
119    #[serde(default)]
120    pub max_ranks: i32,
121    pub name: String,
122    pub description: String,
123    pub icon_file_name: String,
124    #[serde(default)]
125    pub effect_points: Vec<TraitEffectPoints>,
126}
127// docref:end talent-trees-node-entry
128
129#[expect(
130    clippy::trivially_copy_pass_by_ref,
131    reason = "serde skip_serializing_if callbacks receive a reference"
132)]
133const fn is_zero(value: &i32) -> bool {
134    *value == 0
135}
136
137/// How a trait rank's curve value combines with the talent spell effect's own base points.
138#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
139#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
140#[repr(i32)]
141#[non_exhaustive]
142pub enum TraitEffectOperation {
143    /// The rank contributes nothing; the effect keeps its DBC base points.
144    None = -1,
145    /// The rank value replaces the effect's base points.
146    #[default]
147    Set = 0,
148    /// The rank value multiplies the effect's base points.
149    Mul = 1,
150}
151
152impl TraitEffectOperation {
153    #[must_use]
154    pub const fn from_dbc(raw: i32) -> Option<Self> {
155        match raw {
156            -1 => Some(Self::None),
157            0 => Some(Self::Set),
158            1 => Some(Self::Mul),
159            _ => None,
160        }
161    }
162
163    /// Applies this rank's curve `value` to an effect's DBC `base_points`.
164    #[must_use]
165    pub fn apply(self, base_points: f64, value: f64) -> f64 {
166        match self {
167            Self::None => base_points,
168            Self::Set => value,
169            Self::Mul => base_points * value,
170        }
171    }
172}
173
174#[derive(Clone, Debug, Default, Deserialize, Serialize)]
175#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
176pub struct TraitEffectPoints {
177    pub effect_index: i32,
178    #[serde(default)]
179    pub operation: TraitEffectOperation,
180    pub rank_values: Vec<(f64, f64)>,
181}
182
183/// Prerequisite relationship between two nodes.
184#[derive(Clone, Debug, Default, Deserialize, Serialize)]
185#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
186pub struct TraitEdge {
187    pub id: i32,
188    pub from_node_id: i32,
189    pub to_node_id: i32,
190    pub visual_style: i32,
191}
192
193#[derive(Clone, Debug, Default, Deserialize, Serialize)]
194#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
195pub struct TraitSubTree {
196    pub id: i32,
197    pub name: String,
198    pub description: String,
199    pub icon_file_name: String,
200}
201
202#[derive(Clone, Debug, Deserialize, Serialize)]
203#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
204pub struct PointLimits {
205    pub class: i32,
206    pub spec: i32,
207    pub hero: i32,
208}
209
210const DEFAULT_CLASS_POINTS: i32 = 31;
211const DEFAULT_SPEC_POINTS: i32 = 30;
212const DEFAULT_HERO_POINTS: i32 = 10;
213
214impl Default for PointLimits {
215    fn default() -> Self {
216        Self {
217            class: DEFAULT_CLASS_POINTS,
218            spec: DEFAULT_SPEC_POINTS,
219            hero: DEFAULT_HERO_POINTS,
220        }
221    }
222}
223
224#[derive(Clone, Debug, Deserialize, Serialize)]
225pub struct TraitSelection {
226    pub node_id: i32,
227    pub selected: bool,
228    pub ranks_purchased: i32,
229    pub choice_index: Option<u8>,
230}
231
232#[derive(Clone, Debug, Deserialize, Serialize)]
233pub struct TraitTreeWithSelections {
234    #[serde(flatten)]
235    pub tree: TraitTreeFlat,
236    pub selections: Vec<TraitSelection>,
237}