1#![allow(clippy::module_name_repetitions)]
2use std::time::Duration;
3
4use chrono::{DateTime, Local};
5use enum_map::{Enum, EnumMap};
6use num_derive::FromPrimitive;
7use strum::{EnumCount, EnumIter, IntoEnumIterator};
8
9use super::{
10 ArrSkip, CCGet, CFPGet, CSTGet, SFError, ServerTime, items::GemType,
11};
12use crate::{
13 PlayerId,
14 gamestate::{CGet, EnumMapGet},
15 misc::soft_into,
16};
17
18#[derive(Debug, Default, Clone)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub struct Fortress {
22 pub buildings: EnumMap<FortressBuildingType, FortressBuilding>,
25 pub units: EnumMap<FortressUnitType, FortressUnit>,
27 pub resources: EnumMap<FortressResourceType, FortressResource>,
29 pub last_collectable_updated: Option<DateTime<Local>>,
39
40 pub building_max_lvl: u8,
42 pub wall_combat_lvl: u16,
45
46 pub building_upgrade: FortressAction<FortressBuildingType>,
48
49 pub upgrades: u16,
52 pub honor: u32,
54 pub rank: Option<u32>,
56
57 pub gem_search: FortressAction<GemType>,
59
60 pub hall_of_knights_level: u16,
62 pub hall_of_knights_upgrade_price: FortressCost,
65
66 pub attack_target: Option<PlayerId>,
71 pub attack_free_reroll: Option<DateTime<Local>>,
73 pub opponent_reroll_price: u64,
75
76 pub secret_storage_stone: u64,
78 pub secret_storage_wood: u64,
80}
81
82#[derive(Debug, Default, Clone, Copy)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
86pub struct FortressCost {
87 pub time: Duration,
89 pub wood: u64,
91 pub stone: u64,
93 pub silver: u64,
95}
96
97impl FortressCost {
98 pub(crate) fn parse(data: &[i64]) -> Result<FortressCost, SFError> {
99 Ok(FortressCost {
100 time: Duration::from_secs(data.csiget(0, "fortress time", 0)?),
101 silver: data.csiget(1, "silver cost", u64::MAX)?,
103 wood: data.csiget(2, "wood cost", u64::MAX)?,
104 stone: data.csiget(3, "stone cost", u64::MAX)?,
105 })
106 }
107}
108
109#[derive(Debug, Default, Clone)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub struct FortressResource {
113 pub current: u64,
116 pub limit: u64,
119 pub production: FortressProduction,
121}
122
123#[derive(Debug, Default, Clone)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127pub struct FortressProduction {
128 pub last_collectable: u64,
133 pub limit: u64,
136 pub per_hour: u64,
139 pub per_hour_next_lvl: u64,
142}
143
144#[derive(Debug, Clone, Copy, EnumCount, EnumIter, PartialEq, Eq, Enum)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
147#[allow(missing_docs)]
148pub enum FortressResourceType {
149 Wood = 0,
150 Stone = 1,
151 Experience = 2,
152}
153
154#[derive(
156 Debug, Clone, Copy, EnumCount, FromPrimitive, PartialEq, Eq, Enum, EnumIter,
157)]
158#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
159#[allow(missing_docs)]
160pub enum FortressBuildingType {
161 Fortress = 0,
162 LaborersQuarters = 1,
163 WoodcuttersHut = 2,
164 Quarry = 3,
165 GemMine = 4,
166 Academy = 5,
167 ArcheryGuild = 6,
168 Barracks = 7,
169 MagesTower = 8,
170 Treasury = 9,
171 Smithy = 10,
172 Wall = 11,
173}
174
175impl FortressBuildingType {
176 #[must_use]
179 pub fn required_min_fortress_level(&self) -> u16 {
180 match self {
181 FortressBuildingType::Fortress => 0,
182 FortressBuildingType::LaborersQuarters
183 | FortressBuildingType::Quarry
184 | FortressBuildingType::Smithy
185 | FortressBuildingType::WoodcuttersHut => 1,
186 FortressBuildingType::Treasury => 2,
187 FortressBuildingType::GemMine => 3,
188 FortressBuildingType::Barracks | FortressBuildingType::Wall => 4,
189 FortressBuildingType::ArcheryGuild => 5,
190 FortressBuildingType::Academy => 6,
191 FortressBuildingType::MagesTower => 7,
192 }
193 }
194
195 #[must_use]
197 pub fn unit_produced(self) -> Option<FortressUnitType> {
198 match self {
199 FortressBuildingType::Barracks => Some(FortressUnitType::Soldier),
200 FortressBuildingType::MagesTower => {
201 Some(FortressUnitType::Magician)
202 }
203 FortressBuildingType::ArcheryGuild => {
204 Some(FortressUnitType::Archer)
205 }
206 _ => None,
207 }
208 }
209}
210
211#[derive(Debug, Default, Clone)]
213#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
214pub struct FortressUnit {
215 pub level: u16,
217
218 pub count: u16,
220 pub in_training: u16,
222 pub limit: u16,
224 pub training: FortressAction<()>,
226
227 pub upgrade_cost: FortressCost,
229 pub upgrade_next_lvl: u64,
231}
232
233#[derive(Debug, Clone, Copy)]
236#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
237pub struct FortressAction<T> {
238 pub start: Option<DateTime<Local>>,
241 pub finish: Option<DateTime<Local>>,
243 pub cost: FortressCost,
245 pub target: Option<T>,
248}
249
250impl<T> Default for FortressAction<T> {
251 fn default() -> Self {
252 Self {
253 start: None,
254 finish: None,
255 cost: FortressCost::default(),
256 target: None,
257 }
258 }
259}
260
261#[derive(Debug, Clone, Copy, EnumCount, PartialEq, Eq, Enum, EnumIter)]
263#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
264#[allow(missing_docs)]
265pub enum FortressUnitType {
266 Soldier = 0,
267 Magician = 1,
268 Archer = 2,
269}
270
271#[derive(Debug, Default, Clone, Copy)]
274#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
275pub struct FortressBuilding {
276 pub level: u16,
279 pub upgrade_cost: FortressCost,
281}
282
283impl Fortress {
284 #[must_use]
288 pub fn in_use(&self, building_type: FortressBuildingType) -> bool {
289 if let Some(unit_type) = building_type.unit_produced()
291 && let Some(finish) = self.units.get(unit_type).training.finish
292 && finish > Local::now()
293 {
294 return true;
295 }
296
297 if building_type == FortressBuildingType::GemMine
299 && self.gem_search.finish.is_some()
300 {
301 return true;
302 }
303 false
304 }
305
306 #[must_use]
308 pub fn can_build(
309 &self,
310 building_type: FortressBuildingType,
311 available_silver: u64,
312 ) -> bool {
313 let building_info = self.buildings.get(building_type);
314 let fortress_level =
315 self.buildings.get(FortressBuildingType::Fortress).level;
316 let smithy_required_buildings = [
317 FortressBuildingType::ArcheryGuild,
318 FortressBuildingType::Barracks,
319 FortressBuildingType::MagesTower,
320 FortressBuildingType::Wall,
321 ];
322
323 if self.in_use(building_type) {
327 return false;
328 }
329
330 let can_smithy_be_built = smithy_required_buildings
332 .map(|required_building| {
333 self.buildings.get(required_building).level
334 })
335 .iter()
336 .all(|level| *level > 0);
337
338 if matches!(building_type, FortressBuildingType::Smithy)
339 && !can_smithy_be_built
340 {
341 false
343 } else if !matches!(building_type, FortressBuildingType::Fortress)
344 && building_info.level == fortress_level
345 {
346 false
349 } else {
350 let upgrade_cost = building_info.upgrade_cost;
351
352 building_type.required_min_fortress_level() <= fortress_level
354 && self.building_upgrade.target.is_none()
356 && upgrade_cost.stone <= self.resources.get(FortressResourceType::Stone).current
358 && upgrade_cost.wood <= self.resources.get(FortressResourceType::Wood).current
359 && upgrade_cost.silver <= available_silver
360 }
361 }
362
363 pub(crate) fn update(
364 &mut self,
365 data: &[i64],
366 server_time: ServerTime,
367 ) -> Result<(), SFError> {
368 for (idx, typ) in FortressBuildingType::iter().enumerate() {
370 self.buildings.get_mut(typ).level =
371 data.csiget(524 + idx, "building lvl", 0)?;
372 }
373 self.hall_of_knights_level =
374 data.csiget(598, "hall of knights level", 0)?;
375
376 for (idx, typ) in FortressUnitType::iter().enumerate() {
378 let msg = "fortress unit training start";
379 self.units.get_mut(typ).training.start =
380 server_time.convert_to_local(data.cget(550 + idx, msg)?, msg);
381 let msg = "fortress unit training finish";
382 self.units.get_mut(typ).training.finish =
383 server_time.convert_to_local(data.cget(553 + idx, msg)?, msg);
384 }
385
386 #[allow(clippy::enum_glob_use)]
387 {
388 use FortressBuildingType::*;
389 use FortressUnitType::*;
390 self.units.get_mut(Soldier).limit = soft_into(
391 self.buildings.get_mut(Barracks).level * 3,
392 "soldier max count",
393 0,
394 );
395 self.units.get_mut(Magician).limit = soft_into(
396 self.buildings.get_mut(MagesTower).level,
397 "magician max count",
398 0,
399 );
400 self.units.get_mut(Archer).limit = soft_into(
401 self.buildings.get_mut(ArcheryGuild).level * 2,
402 "archer max count",
403 0,
404 );
405
406 self.units.get_mut(Soldier).count =
407 data.csimget(547, "soldier count", 0, |x| x & 0xFFFF)?;
408 self.units.get_mut(Soldier).in_training =
409 data.csimget(548, "soldier in que", 0, |x| x >> 16)?;
410
411 self.units.get_mut(Magician).count =
412 data.csimget(547, "magician count", 0, |x| x >> 16)?;
413 self.units.get_mut(Magician).in_training =
414 data.csimget(549, "magicians in que", 0, |x| x & 0xFFFF)?;
415
416 self.units.get_mut(Archer).count =
417 data.csimget(548, "archer count", 0, |x| x & 0xFFFF)?;
418 self.units.get_mut(Archer).in_training =
419 data.csimget(549, "archer in que", 0, |x| x >> 16)?;
420 }
421
422 for (idx, typ) in FortressResourceType::iter().enumerate() {
424 if typ != FortressResourceType::Experience {
425 self.resources.get_mut(typ).production.per_hour_next_lvl =
426 data.csiget(584 + idx, "max saved next resource", 0)?;
427 }
428
429 self.resources.get_mut(typ).limit =
430 data.csiget(568 + idx, "resource max save", 0)?;
431 self.resources.get_mut(typ).production.last_collectable =
432 data.csiget(562 + idx, "resource in collectable", 0)?;
433 self.resources.get_mut(typ).production.limit =
434 data.csiget(565 + idx, "resource max in store", 0)?;
435 self.resources.get_mut(typ).production.per_hour =
436 data.csiget(574 + idx, "resource per hour", 0)?;
437 }
438
439 self.last_collectable_updated =
440 data.cstget(577, "fortress collection update", server_time)?;
441
442 self.building_upgrade = FortressAction {
443 start: data.cstget(573, "fortress upgrade begin", server_time)?,
444 finish: data.cstget(572, "fortress upgrade end", server_time)?,
445 cost: FortressCost::default(),
446 target: data.cfpget(571, "fortress building upgrade", |x| x - 1)?,
447 };
448
449 self.upgrades = data.csiget(581, "fortress lvl", 0)?;
450 self.honor = data.csiget(582, "fortress honor", 0)?;
451 let fortress_rank: i64 = data.csiget(583, "fortress rank", 0)?;
452 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
453 if fortress_rank > 0 {
454 self.rank = Some(fortress_rank as u32);
455 } else {
456 self.rank = None;
457 }
458
459 self.gem_search.start =
460 data.cstget(596, "gem search start", server_time)?;
461 self.gem_search.finish =
462 data.cstget(595, "gem search end", server_time)?;
463 self.gem_search.target =
464 GemType::parse(data.cget(594, "gem target")?, 0);
465
466 self.attack_target = data.cwiget(587, "fortress enemy")?;
467 self.attack_free_reroll =
468 data.cstget(586, "fortress attack reroll", server_time)?;
469
470 self.secret_storage_wood =
472 data.csiget(698, "secret storage wood", 0)?;
473 self.secret_storage_stone =
474 data.csiget(700, "secret storage stone", 0)?;
475
476 Ok(())
477 }
478
479 pub(crate) fn update_unit_prices(
480 &mut self,
481 data: &[i64],
482 ) -> Result<(), SFError> {
483 for (i, typ) in FortressUnitType::iter().enumerate() {
484 self.units.get_mut(typ).training.cost =
485 FortressCost::parse(data.skip(i * 4, "unit prices")?)?;
486 }
487 Ok(())
488 }
489
490 pub(crate) fn update_unit_upgrade_info(
491 &mut self,
492 data: &[i64],
493 ) -> Result<(), SFError> {
494 for (i, typ) in FortressUnitType::iter().enumerate() {
495 self.units.get_mut(typ).upgrade_next_lvl =
496 data.csiget(i * 3, "unit next lvl", 0)?;
497 self.units.get_mut(typ).upgrade_cost.wood =
498 data.csiget(1 + i * 3, "wood price next unit lvl", 0)?;
499 self.units.get_mut(typ).upgrade_cost.stone =
500 data.csiget(2 + i * 3, "stone price next unit lvl", 0)?;
501 }
502 Ok(())
503 }
504
505 pub(crate) fn update_levels(
506 &mut self,
507 data: &[i64],
508 ) -> Result<(), SFError> {
509 self.units.get_mut(FortressUnitType::Soldier).level =
510 data.csiget(1, "soldier level", 0)?;
511 self.units.get_mut(FortressUnitType::Magician).level =
512 data.csiget(2, "magician level", 0)?;
513 self.units.get_mut(FortressUnitType::Archer).level =
514 data.csiget(3, "archer level", 0)?;
515 Ok(())
516 }
517
518 pub(crate) fn update_prices(
519 &mut self,
520 data: &[i64],
521 ) -> Result<(), SFError> {
522 for (i, typ) in FortressBuildingType::iter().enumerate() {
523 self.buildings.get_mut(typ).upgrade_cost =
524 FortressCost::parse(data.skip(i * 4, "fortress unit prices")?)?;
525 }
526 self.gem_search.cost =
527 FortressCost::parse(data.skip(48, "gem_search_cost")?)?;
528 Ok(())
529 }
530}