halbu/character/mercenary/
mod.rs1use crate::utils::u16_from;
2use crate::utils::u32_from;
3use crate::ParseHardError;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::ops::Range;
7
8mod tests;
9
10enum Section {
11 IsDead,
12 Id,
13 NameId,
14 VariantId,
15 Experience,
16}
17
18impl Section {
19 const fn range(self) -> Range<usize> {
20 match self {
21 Section::IsDead => 0..2,
22 Section::Id => 2..6,
23 Section::NameId => 6..8,
24 Section::VariantId => 8..10,
25 Section::Experience => 10..14,
26 }
27 }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31enum MercenaryType {
32 Rogue,
33 DesertMercenary,
34 IronWolf,
35 Barbarian,
36}
37
38fn mercenary_type_for_variant_id(variant_id: u16) -> Option<MercenaryType> {
39 match variant_id {
40 0..=5 => Some(MercenaryType::Rogue),
41 6..=14 | 30..=35 => Some(MercenaryType::DesertMercenary),
42 15..=23 => Some(MercenaryType::IronWolf),
43 24..=29 | 36..=38 => Some(MercenaryType::Barbarian),
44 _ => None,
45 }
46}
47
48fn mercenary_name_count_for_type(mercenary_type: MercenaryType) -> usize {
49 match mercenary_type {
50 MercenaryType::Rogue => 41,
51 MercenaryType::DesertMercenary => 21,
52 MercenaryType::IronWolf => 20,
53 MercenaryType::Barbarian => 67,
54 }
55}
56
57pub(crate) fn mercenary_name_count_for_variant_id(variant_id: u16) -> Option<usize> {
59 mercenary_type_for_variant_id(variant_id).map(mercenary_name_count_for_type)
60}
61
62pub(crate) fn xp_rate_for_variant_id(variant_id: u16) -> Option<u32> {
64 match variant_id {
65 0 => Some(100),
66 1 => Some(105),
67 2 => Some(110),
68 3 => Some(115),
69 4 => Some(120),
70 5 => Some(125),
71 6..=8 => Some(110),
72 9..=11 => Some(120),
73 12..=14 => Some(130),
74 15 | 17 => Some(110),
75 16 => Some(120),
76 18 | 20 => Some(120),
77 19 => Some(130),
78 21 | 23 => Some(130),
79 22 => Some(140),
80 24 | 25 => Some(120),
81 26 | 27 => Some(130),
82 28 | 29 => Some(140),
83 30..=32 => Some(120),
84 33..=35 => Some(130),
85 36 => Some(120),
86 37 => Some(130),
87 38 => Some(140),
88 _ => None,
89 }
90}
91
92pub(crate) fn level_from_experience(experience: u32, xp_rate: u32) -> u8 {
96 let scaled_experience = experience / xp_rate;
97 let guess = (scaled_experience as f64).cbrt().floor() as u8;
98
99 if scaled_experience < u32::from(guess) * u32::from(guess) * u32::from(guess + 1) {
100 guess.saturating_sub(1)
101 } else {
102 guess
103 }
104}
105
106#[derive(Default, PartialEq, Eq, Debug, Copy, Clone, Serialize, Deserialize)]
107pub struct Mercenary {
108 pub is_dead: bool,
109 pub id: u32,
110 pub name_id: u16,
111 pub variant_id: u16,
112 pub experience: u32,
113}
114
115impl fmt::Display for Mercenary {
116 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
117 write!(
118 f,
119 "Dead: {0}, ID: {1}, Name_ID: {2}, Variant: {3}, XP: {4}",
120 self.is_dead, self.id, self.name_id, self.variant_id, self.experience
121 )
122 }
123}
124
125impl Mercenary {
126 pub(crate) fn has_data_without_hire(&self) -> bool {
127 !self.is_hired()
128 && (self.is_dead || self.name_id != 0 || self.variant_id != 0 || self.experience != 0)
129 }
130
131 pub fn write(&self) -> [u8; 14] {
132 let mut bytes: [u8; 14] = [0x00; 14];
133 if !self.is_hired() {
134 return bytes;
135 }
136
137 bytes[Section::IsDead.range()].copy_from_slice(match self.is_dead {
138 true => &[0x01, 0x00],
139 false => &[0x00, 0x00],
140 });
141
142 bytes[Section::Id.range()].copy_from_slice(&self.id.to_le_bytes());
143 bytes[Section::NameId.range()].copy_from_slice(&self.name_id.to_le_bytes());
144 bytes[Section::VariantId.range()].copy_from_slice(&self.variant_id.to_le_bytes());
145 bytes[Section::Experience.range()].copy_from_slice(&self.experience.to_le_bytes());
146 bytes
147 }
148
149 pub fn parse(data: &[u8]) -> Result<Mercenary, ParseHardError> {
150 if data.len() < 14 {
151 return Err(ParseHardError {
152 message: format!(
153 "Mercenary section is truncated: expected 14 bytes, found {}.",
154 data.len()
155 ),
156 });
157 }
158
159 let mut mercenary: Mercenary = Mercenary::default();
160 if u16_from(&data[Section::IsDead.range()], "mercenary.is_dead")? != 0 {
161 mercenary.is_dead = true;
162 }
163
164 mercenary.id = u32_from(&data[Section::Id.range()], "mercenary.id")?;
165 mercenary.variant_id = u16_from(&data[Section::VariantId.range()], "mercenary.variant_id")?;
166 mercenary.name_id = u16_from(&data[Section::NameId.range()], "mercenary.name_id")?;
167 mercenary.experience =
168 u32_from(&data[Section::Experience.range()], "mercenary.experience")?;
169
170 Ok(mercenary)
171 }
172
173 pub fn is_hired(&self) -> bool {
174 self.id != 0u32
175 }
176}