Skip to main content

halbu/character/mercenary/
mod.rs

1use 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
57/// Return the mercenary name count for a known variant id.
58pub(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
62/// Return the XP rate for a known mercenary variant id.
63pub(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
92/// Resolve a mercenary level from current experience.
93///
94/// This returns `0` when the experience is below level 1.
95pub(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}