1use serde::{Deserialize, Serialize};
2use wowlab_engine_macros::CopyInsert;
3
4#[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#[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#[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#[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#[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#[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 #[serde(default)]
113 pub override_spell_id: i32,
114 #[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#[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#[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 None = -1,
145 #[default]
147 Set = 0,
148 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 #[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#[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}