1use chrono::{DateTime, Local};
2use num_traits::FromPrimitive;
3
4use super::{items::*, *};
5use crate::PlayerId;
6
7#[derive(Debug, Default, Clone)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub struct Arena {
11 pub enemy_ids: [PlayerId; 3],
14 pub next_free_fight: Option<DateTime<Local>>,
16 pub fights_for_xp: u8,
19}
20
21#[derive(Debug, Default, Clone)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct Fight {
26 pub group_attacker_name: Option<String>,
29 pub group_attacker_id: Option<u32>,
31
32 pub group_defender_name: Option<String>,
35 pub group_defender_id: Option<u32>,
37
38 pub fights: Vec<SingleFight>,
41 pub has_player_won: bool,
43 pub silver_change: i64,
45 pub xp_change: u64,
47 pub mushroom_change: u8,
49 pub honor_change: i64,
51 pub rank_pre_fight: u32,
53 pub rank_post_fight: u32,
55 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 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#[derive(Debug, Default, Clone)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
108pub struct SingleFight {
109 pub winner_id: PlayerId,
111 pub fighter_a: Option<Fighter>,
114 pub fighter_b: Option<Fighter>,
116 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 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 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#[derive(Debug, Clone)]
175#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
176pub struct Fighter {
177 pub typ: FighterTyp,
179 pub id: i64,
182 pub name: Option<String>,
184 pub level: u32,
186 pub life: u32,
188 pub attributes: EnumMap<AttributeType, u32>,
190 pub class: Class,
192}
193
194impl Fighter {
195 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 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 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#[derive(Debug, Clone, Copy)]
263#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
264pub struct FightAction {
265 pub acting_id: i64,
267 pub other_new_life: i64,
271 pub action: FightActionType,
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
278#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
279#[non_exhaustive]
280pub enum FightActionType {
281 Attack,
283 MushroomCatapult,
285 Blocked,
287 Evaded,
289 MinionAttack,
291 MinionAttackBlocked,
293 MinionAttackEvaded,
295 MinionCrit,
297 SummonSpecial,
299 Unknown,
302}
303
304impl FightActionType {
305 pub(crate) fn parse(val: u32) -> FightActionType {
306 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
324#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
325pub enum FighterTyp {
326 #[default]
328 Player,
329 Monster(u16),
331 Companion(CompanionClass),
333 FortressPillager,
335 FortressWall,
337 UnderworldMinion,
339 Pet,
341}