1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
/// Represents a parsed path to a stat, potentially including its name, part, tag, and target source.
///
/// Stat paths are strings used throughout the system to identify specific stats or their components.
/// Examples:
/// - `"Life"`: Refers to the base "Life" stat.
/// - `"Damage.base"`: Refers to the "base" part of the "Damage" stat.
/// - `"Damage.increased.123"`: Refers to the "increased" part of "Damage", specifically with tag `123`.
/// - `"Strength@Player"`: Refers to the "Strength" stat from the source aliased as "Player".
/// - `"Armor.base@EnemyTarget"`: Refers to the "base" part of "Armor" from source "EnemyTarget".
///
/// The `StatPath` struct holds these parsed components for easier access.
/// It is lifetime-parameterized (`\'a`) as it borrows string slices from the original path string.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StatPath<'a> {
/// The original, unparsed string slice from which this `StatPath` was created.
pub full_path: &'a str,
/// The primary name of the stat (e.g., "Strength" in "Strength.base.1@Player").
/// This is the first segment of the path before any `.` or `@`.
pub name: &'a str,
/// An optional part of the stat, typically following the name (e.g., "base" in "Damage.base").
/// If the segment after the name is a numerical tag, `part` will be `None`.
pub part: Option<&'a str>,
/// An optional numerical tag associated with the stat or its part.
/// This can be the segment directly after the `name` (if numerical), or after the `part` (if numerical).
/// Example: `123` in `"Damage.increased.123"` or `"Buff.42"`.
pub tag: Option<u32>,
/// An optional source alias, indicating that the stat originates from a different entity or context.
/// This is the segment following an `@` symbol (e.g., "Player" in `"Life@Player"`).
pub target: Option<&'a str>,
}
/// Parses a segment string to determine if it represents a u32 number.
fn parse_segment_as_numerical_tag(segment: &str) -> Option<u32> {
// Trim whitespace to be a bit more lenient, though typically paths don't have spaces.
segment.trim().parse::<u32>().ok()
}
impl<'a> StatPath<'a> {
/// Parses a string slice into a `StatPath`, dissecting it into its constituent components.
///
/// The parsing logic is as follows:
/// 1. Checks for and extracts a target alias if an `@` symbol is present (e.g., `"StatName@TargetAlias"`).
/// The part before `@` becomes the path to parse for name, part, and tag.
/// 2. The `name` is the first segment of the path (before the first `.` if any).
/// 3. If a second segment exists after the `name`:
/// a. If this segment can be parsed as a `u32`, it becomes the `tag`, and `part` remains `None`.
/// b. Otherwise, this segment becomes the `part`.
/// 4. If a `part` was identified (from step 3b), and a third segment exists:
/// a. If this third segment can be parsed as a `u32`, it becomes the `tag`.
/// b. Otherwise, it is ignored for the purpose of `StatPath` fields.
/// 5. Any segments beyond these are not captured in the distinct fields of `StatPath` but are part of `full_path`.
///
/// # Examples
///
/// ```
/// use bevy_gauge::prelude::StatPath; // Adjust import path as needed
///
/// let p1 = StatPath::parse("Damage.base.10@Player");
/// assert_eq!(p1.name, "Damage");
/// assert_eq!(p1.part, Some("base"));
/// assert_eq!(p1.tag, Some(10));
/// assert_eq!(p1.target, Some("Player"));
///
/// let p2 = StatPath::parse("Damage.25");
/// assert_eq!(p2.name, "Damage");
/// assert_eq!(p2.part, None);
/// assert_eq!(p2.tag, Some(25));
/// assert_eq!(p2.target, None);
///
/// let p3 = StatPath::parse("Speed@MyCharacter");
/// assert_eq!(p3.name, "Speed");
/// assert_eq!(p3.part, None);
/// assert_eq!(p3.tag, None);
/// assert_eq!(p3.target, Some("MyCharacter"));
///
/// let p4 = StatPath::parse("Mana");
/// assert_eq!(p4.name, "Mana");
/// assert_eq!(p4.part, None);
/// assert_eq!(p4.tag, None);
/// assert_eq!(p4.target, None);
/// ```
///
/// # Arguments
///
/// * `s`: The string slice to parse.
///
/// # Returns
///
/// A `StatPath` instance representing the parsed components of the input string.
pub fn parse(s: &'a str) -> Self {
let full_path = s;
let mut target_val: Option<&'a str> = None;
let mut path_to_parse: &'a str = s;
if let Some((base_candidate, source_candidate)) = path_to_parse.rsplit_once('@') {
if !source_candidate.is_empty() {
target_val = Some(source_candidate);
}
path_to_parse = base_candidate;
}
if path_to_parse.is_empty() && target_val.is_some() {
// This case handles if the input was just "@SomeTarget"
// or if after splitting, base_candidate was empty (e.g. "@Target")
return Self {
full_path,
name: "", // No actual stat name part
part: None,
tag: None,
target: target_val,
};
} else if path_to_parse.is_empty() {
// Input was completely empty string or only "@"
return Self {
full_path,
name: "",
part: None,
tag: None,
target: None, // or target_val which would be None if path_to_parse is empty from just "@"
};
}
// Handle $[...] auto-generated names
let all_segments: Vec<&'a str> = if path_to_parse.starts_with("$[") {
if let Some(bracket_end) = path_to_parse.find(']') {
let name_part = &path_to_parse[..=bracket_end]; // "$[Damage.range.min]"
let remaining = &path_to_parse[bracket_end + 1..]; // ".increased.FIRE"
let mut segments = vec![name_part];
if remaining.starts_with('.') && remaining.len() > 1 {
segments.extend(remaining[1..].split('.'));
}
segments
} else {
// Malformed $[ without ], fall back to normal parsing
path_to_parse.split('.').collect()
}
} else {
// Normal parsing (unchanged)
path_to_parse.split('.').collect()
};
let mut name_val: &'a str = "";
let mut part_val: Option<&'a str> = None;
let mut tag_val: Option<u32> = None;
// name_val is the first segment. If path_to_parse was ".", all_segments is ["", ""].
// If path_to_parse was "", all_segments is [""] (but caught by is_empty above).
if let Some(first_segment) = all_segments.get(0) {
name_val = first_segment;
}
// else name_val remains "", which is correct if all_segments was unexpectedly empty
// despite path_to_parse not being empty (highly unlikely with split).
if let Some(s1) = all_segments.get(1).cloned() { // Segment after name
if let Some(parsed_tag) = parse_segment_as_numerical_tag(s1) {
tag_val = Some(parsed_tag);
// part_val remains None as s1 was consumed as a tag.
// Any s2 (third segment) is ignored if s1 is a tag.
} else {
// s1 is not a numerical tag, so it's a part.
part_val = Some(s1);
if let Some(s2) = all_segments.get(2).cloned() { // Segment after part
if let Some(parsed_tag_s2) = parse_segment_as_numerical_tag(s2) {
tag_val = Some(parsed_tag_s2);
}
// Else s2 is not a numerical tag; it's ignored (as part is only s1).
}
}
}
// If only one segment (name_val), part_val and tag_val remain None.
Self {
full_path,
name: name_val,
part: part_val,
tag: tag_val,
target: target_val,
}
}
/// Returns the original, unparsed full string path.
pub fn full_path(&self) -> &'a str { self.full_path }
/// Returns the primary name of the stat.
pub fn name(&self) -> &'a str { self.name }
/// Returns the optional part of the stat.
pub fn part(&self) -> Option<&'a str> { self.part }
/// Returns the optional numerical tag.
pub fn tag(&self) -> Option<u32> { self.tag }
/// Returns the optional target alias.
pub fn target(&self) -> Option<&'a str> { self.target }
/// Returns `true` if the stat path includes a target alias.
pub fn has_target(&self) -> bool { self.target.is_some() }
/// Returns `true` if the stat path includes a numerical tag.
pub fn has_tags(&self) -> bool { self.tag.is_some() }
/// Reconstructs the stat path string without the target alias, if one was present.
/// Example: `"Damage.base.10@Player"` becomes `"Damage.base.10"`.
pub fn without_target_as_string(&self) -> String {
let mut parts = Vec::new();
parts.push(self.name.to_string());
if let Some(p) = self.part { parts.push(p.to_string()); }
if let Some(t) = self.tag { parts.push(t.to_string()); }
parts.join(".")
}
}
impl<'a> ToString for StatPath<'a> {
fn to_string(&self) -> String {
self.full_path.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auto_generated_stat_basic() {
let p = StatPath::parse("$[Damage.range.min]");
assert_eq!(p.name, "$[Damage.range.min]");
assert_eq!(p.part, None);
assert_eq!(p.tag, None);
assert_eq!(p.target, None);
}
#[test]
fn test_auto_generated_stat_with_part() {
let p = StatPath::parse("$[Damage.range.min].increased");
assert_eq!(p.name, "$[Damage.range.min]");
assert_eq!(p.part, Some("increased"));
assert_eq!(p.tag, None);
assert_eq!(p.target, None);
}
#[test]
fn test_auto_generated_stat_with_tag() {
let p = StatPath::parse("$[Damage.range.min].123");
assert_eq!(p.name, "$[Damage.range.min]");
assert_eq!(p.part, None);
assert_eq!(p.tag, Some(123));
assert_eq!(p.target, None);
}
#[test]
fn test_auto_generated_stat_with_part_and_tag() {
let p = StatPath::parse("$[Damage.range.min].increased.456");
assert_eq!(p.name, "$[Damage.range.min]");
assert_eq!(p.part, Some("increased"));
assert_eq!(p.tag, Some(456));
assert_eq!(p.target, None);
}
#[test]
fn test_auto_generated_stat_with_target() {
let p = StatPath::parse("$[Damage.range.min]@parent");
assert_eq!(p.name, "$[Damage.range.min]");
assert_eq!(p.part, None);
assert_eq!(p.tag, None);
assert_eq!(p.target, Some("parent"));
}
#[test]
fn test_auto_generated_stat_complex() {
let p = StatPath::parse("$[Damage.range.min].increased.FIRE@parent");
assert_eq!(p.name, "$[Damage.range.min]");
assert_eq!(p.part, Some("increased"));
assert_eq!(p.tag, None); // FIRE is not numerical
assert_eq!(p.target, Some("parent"));
}
#[test]
fn test_auto_generated_malformed_fallback() {
// Missing closing bracket should fall back to normal parsing
let p = StatPath::parse("$[Damage.range.min");
assert_eq!(p.name, "$[Damage");
assert_eq!(p.part, Some("range"));
assert_eq!(p.tag, None);
assert_eq!(p.target, None);
}
#[test]
fn test_normal_parsing_unchanged() {
// Ensure normal parsing still works
let p = StatPath::parse("Damage.increased.123@target");
assert_eq!(p.name, "Damage");
assert_eq!(p.part, Some("increased"));
assert_eq!(p.tag, Some(123));
assert_eq!(p.target, Some("target"));
}
}