sf_api/gamestate/
arena.rs

1use chrono::{DateTime, Local};
2use num_traits::FromPrimitive;
3
4use super::{items::*, *};
5use crate::PlayerId;
6
7/// The arena, that a player can fight other players in
8#[derive(Debug, Default, Clone)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub struct Arena {
11    /// The enemies currently available in the arena. You have to fetch the
12    /// full player info before fighting them, as you need their name
13    pub enemy_ids: [PlayerId; 3],
14    /// The time at which the player will be able to fight for free again
15    pub next_free_fight: Option<DateTime<Local>>,
16    /// The amount of fights this character has already fought today, that
17    /// gave xp. 0-10
18    pub fights_for_xp: u8,
19}
20
21/// A complete fight, which can be between multiple fighters for guild/tower
22/// fights
23#[derive(Debug, Default, Clone)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct Fight {
26    /// The name of the attacking player for pet battles, or the name of the
27    /// attacking guild in guild battles
28    pub group_attacker_name: Option<String>,
29    /// Either the player or guild id depending on pet/guild fight
30    pub group_attacker_id: Option<u32>,
31
32    /// The name of the attacking player for pet battles, or the name of the
33    /// attacking guild in guild battles
34    pub group_defender_name: Option<String>,
35    /// Either the player or guild id depending on pet/guild fight
36    pub group_defender_id: Option<u32>,
37
38    /// The 1on1 fights within a larger fight, that end with one of the
39    /// contestants defeated
40    pub fights: Vec<SingleFight>,
41    /// Whether the fight was won by the player.
42    pub has_player_won: bool,
43    /// The amount of money, that changed from a players perspective
44    pub silver_change: i64,
45    /// The amount of experience, that changed from a players perspective
46    pub xp_change: u64,
47    /// The amount of mushrooms the player got after this fight
48    pub mushroom_change: u8,
49    /// How much this fight changed the players honor by
50    pub honor_change: i64,
51    /// The rank before this fight
52    pub rank_pre_fight: u32,
53    /// The rank after this fight
54    pub rank_post_fight: u32,
55    /// The item this fight gave the player (if any)
56    pub item_won: Option<Item>,
57}
58
59impl Fight {
60    pub(crate) fn update_result(
61        &mut self,
62        data: &[i64],
63        server_time: ServerTime,
64    ) -> Result<(), SFError> {
65        self.has_player_won = data.cget(0, "has_player_won")? != 0;
66        self.silver_change = data.cget(2, "fight silver change")?;
67
68        if data.len() < 20 {
69            // Skip underworld
70            return Ok(());
71        }
72
73        self.xp_change = data.csiget(3, "fight xp", 0)?;
74        self.mushroom_change = data.csiget(4, "fight mushrooms", 0)?;
75        self.honor_change = data.cget(5, "fight honor")?;
76
77        self.rank_pre_fight = data.csiget(7, "fight rank pre", 0)?;
78        self.rank_post_fight = data.csiget(8, "fight rank post", 0)?;
79        let item = data.skip(9, "fight item")?;
80        self.item_won = Item::parse(item, server_time)?;
81        Ok(())
82    }
83
84    pub(crate) fn update_groups(&mut self, val: &str) {
85        let mut groups = val.split(',');
86
87        let (Some(aid), Some(did), Some(aname), Some(dname)) = (
88            groups.next().and_then(|a| a.parse().ok()),
89            groups.next().and_then(|a| a.parse().ok()),
90            groups.next(),
91            groups.next(),
92        ) else {
93            warn!("Invalid fight group: {val}");
94            return;
95        };
96
97        self.group_attacker_id = Some(aid);
98        self.group_defender_id = Some(did);
99        self.group_attacker_name = Some(aname.to_string());
100        self.group_defender_name = Some(dname.to_string());
101    }
102}
103
104/// This is a single fight between two fighters, which ends when one of them is
105/// at <= 0 health
106#[derive(Debug, Default, Clone)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
108pub struct SingleFight {
109    /// The ID of the player, that won.
110    pub winner_id: PlayerId,
111    /// The stats of the first fighter. Typically the player, if the fight was
112    /// started by them
113    pub fighter_a: Option<Fighter>,
114    /// The stats of the first fighter
115    pub fighter_b: Option<Fighter>,
116    /// The action this fight involved. Note that this will likely be changed
117    /// in the future, as is it hard to interpret
118    pub actions: Vec<FightAction>,
119}
120
121impl SingleFight {
122    pub(crate) fn update_fighters(&mut self, data: &str) {
123        let data = data.split('/').collect::<Vec<_>>();
124        if data.len() < 60 {
125            self.fighter_a = None;
126            self.fighter_b = None;
127            warn!("Fighter response too short");
128            return;
129        }
130        // FIXME: IIRC this should probably be split(data.len() / 2) instead
131        let (fighter_a, fighter_b) = data.split_at(47);
132        self.fighter_a = Fighter::parse(fighter_a);
133        self.fighter_b = Fighter::parse(fighter_b);
134    }
135
136    pub(crate) fn update_rounds(
137        &mut self,
138        data: &str,
139        fight_version: u32,
140    ) -> Result<(), SFError> {
141        self.actions.clear();
142
143        if fight_version > 1 {
144            // TODO: Actually parse this
145            return Ok(());
146        }
147        let mut iter = data.split(',');
148        while let (Some(player_id), Some(damage_typ), Some(new_life)) =
149            (iter.next(), iter.next(), iter.next())
150        {
151            let action =
152                warning_from_str(damage_typ, "fight action").unwrap_or(0);
153
154            self.actions.push(FightAction {
155                acting_id: player_id.parse().map_err(|_| {
156                    SFError::ParsingError("action pid", player_id.to_string())
157                })?,
158                action: FightActionType::parse(action),
159                other_new_life: new_life.parse().map_err(|_| {
160                    SFError::ParsingError(
161                        "action new life",
162                        player_id.to_string(),
163                    )
164                })?,
165            });
166        }
167
168        Ok(())
169    }
170}
171
172/// A participant in a fight. Can be anything, that shows up in the battle
173/// screen from the player to a fortress Wall
174#[derive(Debug, Clone)]
175#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
176pub struct Fighter {
177    /// The type of the fighter
178    pub typ: FighterTyp,
179    /// The raw id of the fighter. This is <= 0 for monsters & companions and
180    /// equivalent to the player id for players (>0)
181    pub id: i64,
182    /// The name of the fighter, if it is a player
183    pub name: Option<String>,
184    /// The level of the fighter
185    pub level: u32,
186    /// The amount of hp this fighter has at the start of a battle
187    pub life: u32,
188    /// The total attributes this fighter has
189    pub attributes: EnumMap<AttributeType, u32>,
190    /// The class of the fighter
191    pub class: Class,
192}
193
194impl Fighter {
195    // TODO: Make this return Result?
196    pub(crate) fn parse(data: &[&str]) -> Option<Fighter> {
197        let fighter_typ: i64 = data.cfsget(5, "fighter typ").ok()??;
198
199        let mut fighter_type = match fighter_typ {
200            -391 => FighterTyp::Companion(CompanionClass::Warrior),
201            -392 => FighterTyp::Companion(CompanionClass::Mage),
202            -393 => FighterTyp::Companion(CompanionClass::Scout),
203            1.. => FighterTyp::Player,
204            x => {
205                let monster_id = soft_into(-x, "monster_id", 0);
206                FighterTyp::Monster(monster_id)
207            }
208        };
209
210        let mut attributes = EnumMap::default();
211        let raw_atrs =
212            parse_vec(data.get(10..15)?, "fighter attributes", |a| {
213                a.parse().ok()
214            })
215            .ok()?;
216        update_enum_map(&mut attributes, &raw_atrs);
217
218        let class: i32 = data.cfsget(27, "fighter class").ok().flatten()?;
219        let class: Class = FromPrimitive::from_i32(class - 1)?;
220
221        let id = data.cfsget(5, "fighter id").ok()?.unwrap_or_default();
222
223        let name = match data.cget(6, "fighter name").ok()?.parse::<i64>() {
224            Ok(-770..=-740) => {
225                // This range might be too large
226                fighter_type = FighterTyp::FortressWall;
227                None
228            }
229            Ok(-712) => {
230                fighter_type = FighterTyp::FortressPillager;
231                None
232            }
233            Ok(..=-1) => None,
234            Ok(0) => {
235                let id = data.cget(15, "fighter uwm").ok()?;
236                // No idea if this correct
237                if ["-910", "-935", "-933", "-924"].contains(&id) {
238                    fighter_type = FighterTyp::UnderworldMinion;
239                }
240                None
241            }
242            Ok(pid) if pid == id && fighter_type == FighterTyp::Player => {
243                fighter_type = FighterTyp::Pet;
244                None
245            }
246            _ => Some(data.cget(6, "fighter name").ok()?.to_string()),
247        };
248
249        Some(Fighter {
250            typ: fighter_type,
251            id,
252            name,
253            level: data.cfsget(7, "fighter lvl").ok()??,
254            life: data.cfsget(8, "fighter life").ok()??,
255            attributes,
256            class,
257        })
258    }
259}
260
261/// One round (action) in a fight. This is mostly just one attack
262#[derive(Debug, Clone, Copy)]
263#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
264pub struct FightAction {
265    /// The id of the fighter, that does the action
266    pub acting_id: i64,
267    /// The new current life of the fighter, that was hit. Note that this may
268    /// be 0 for actions, like spawning minions, that dont have a target
269    /// and thus no target health.
270    pub other_new_life: i64,
271    /// The action, that the active side does
272    pub action: FightActionType,
273}
274
275/// An action in a fight. In the official client this determines the animation,
276/// that gets played
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
278#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
279#[non_exhaustive]
280pub enum FightActionType {
281    /// A simple attack with the normal weapon
282    Attack,
283    /// One shot from a loaded mushroom catapult in a guild battle
284    MushroomCatapult,
285    /// The last action was blocked
286    Blocked,
287    /// The last action was evaded
288    Evaded,
289    /// The summoned minion attacks
290    MinionAttack,
291    /// The summoned minion blocked the last attack
292    MinionAttackBlocked,
293    /// The summoned minion evaded the last attack
294    MinionAttackEvaded,
295    /// The summoned minion was crit
296    MinionCrit,
297    /// Plays the harp, or summons a friendly minion
298    SummonSpecial,
299    /// I have not checked all possible battle types, so whatever action I have
300    /// missed will be parsed as this
301    Unknown,
302}
303
304impl FightActionType {
305    pub(crate) fn parse(val: u32) -> FightActionType {
306        // FIXME: Is this missing crit?
307        match val {
308            0 | 1 => FightActionType::Attack,
309            2 => FightActionType::MushroomCatapult,
310            3 => FightActionType::Blocked,
311            4 => FightActionType::Evaded,
312            5 => FightActionType::MinionAttack,
313            6 => FightActionType::MinionAttackBlocked,
314            7 => FightActionType::MinionAttackEvaded,
315            25 => FightActionType::MinionCrit,
316            200..=250 => FightActionType::SummonSpecial,
317            _ => FightActionType::Unknown,
318        }
319    }
320}
321
322/// The type of the participant in a fight
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
324#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
325pub enum FighterTyp {
326    /// Not just the own player, but any player on the server
327    #[default]
328    Player,
329    /// A generic monster, or dungeon boss with its `monster_id`
330    Monster(u16),
331    /// One of the players companions
332    Companion(CompanionClass),
333    /// A pillager in a fortress attack
334    FortressPillager,
335    /// The wall in a fortress attack
336    FortressWall,
337    /// A minion in an underworld lure battle
338    UnderworldMinion,
339    /// A pet
340    Pet,
341}