1use chrono::{DateTime, Local};
2use log::{error, warn};
3use num_derive::FromPrimitive;
4use num_traits::FromPrimitive;
5
6use super::{
7 CCGet, CFPGet, CSTGet, ExpeditionSetting, SFError, ServerTime, items::Item,
8};
9use crate::{
10 command::{DiceReward, DiceType},
11 gamestate::rewards::Reward,
12 misc::soft_into,
13};
14
15#[derive(Debug, Clone, Default)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub struct Tavern {
19 pub quests: [Quest; 3],
21 #[doc(alias = "alu")]
23 pub thirst_for_adventure_sec: u32,
24 pub mushroom_skip_allowed: bool,
26 pub beer_drunk: u8,
28 pub quicksand_glasses: u32,
30 pub current_action: CurrentAction,
32 pub guard_wage: u64,
34 pub toilet: Option<Toilet>,
36 pub dice_game: DiceGame,
38 pub expeditions: ExpeditionsEvent,
40 pub questing_preference: ExpeditionSetting,
43 pub gamble_result: Option<GambleResult>,
45 pub beer_max: u8,
47}
48
49#[derive(Debug, Clone, Default)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub struct ExpeditionsEvent {
53 pub start: Option<DateTime<Local>>,
55 pub end: Option<DateTime<Local>>,
57 pub available: Vec<AvailableExpedition>,
59 pub(crate) active: Option<Expedition>,
62}
63
64impl ExpeditionsEvent {
65 #[must_use]
68 pub fn is_event_ongoing(&self) -> bool {
69 let now = Local::now();
70 matches!((self.start, self.end), (Some(start), Some(end)) if end > now && start < now)
71 }
72
73 #[must_use]
77 pub fn active(&self) -> Option<&Expedition> {
78 self.active.as_ref().filter(|a| !a.is_finished())
79 }
80
81 #[must_use]
85 pub fn active_mut(&mut self) -> Option<&mut Expedition> {
86 self.active.as_mut().filter(|a| !a.is_finished())
87 }
88}
89
90#[derive(Debug, Clone, Default)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
93pub struct DiceGame {
94 pub remaining: u8,
96 pub next_free: Option<DateTime<Local>>,
98 pub current_dice: Vec<DiceType>,
101 pub reward: Option<DiceReward>,
103}
104
105#[derive(Debug, Clone)]
109#[allow(missing_docs)]
110pub enum AvailableTasks<'a> {
111 Quests(&'a [Quest; 3]),
112 Expeditions(&'a [AvailableExpedition]),
113}
114
115impl Tavern {
116 #[must_use]
122 pub fn is_idle(&self) -> bool {
123 match self.current_action {
124 CurrentAction::Idle => true,
125 CurrentAction::Expedition => self.expeditions.active.is_none(),
126 _ => false,
127 }
128 }
129
130 #[must_use]
135 pub fn available_tasks(&self) -> AvailableTasks<'_> {
136 if self.questing_preference == ExpeditionSetting::PreferExpeditions
137 && self.expeditions.is_event_ongoing()
138 {
139 AvailableTasks::Expeditions(&self.expeditions.available)
140 } else {
141 AvailableTasks::Quests(&self.quests)
142 }
143 }
144
145 #[must_use]
148 pub fn can_change_questing_preference(&self) -> bool {
149 self.thirst_for_adventure_sec == 6000 && self.beer_drunk == 0
150 }
151}
152
153#[derive(Debug, Default, Clone, PartialEq, Eq)]
155#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
156pub struct Quest {
157 pub base_length: u32,
159 pub base_silver: u32,
161 pub base_experience: u32,
163 pub item: Option<Item>,
165 pub location_id: Location,
167 pub monster_id: u16,
169}
170
171#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, FromPrimitive, Hash)]
173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
174#[allow(missing_docs)]
175pub enum Location {
176 #[default]
177 SprawlingJungle = 1,
178 SkullIsland,
179 EvernightForest,
180 StumbleSteppe,
181 ShadowrockMountain,
182 SplitCanyon,
183 BlackWaterSwamp,
184 FloodedCaldwell,
185 TuskMountain,
186 MoldyForest,
187 Nevermoor,
188 BustedLands,
189 Erogenion,
190 Magmaron,
191 SunburnDesert,
192 Gnarogrim,
193 Northrunt,
194 BlackForest,
195 Maerwynn,
196 PlainsOfOzKorr,
197 RottenLands,
198}
199
200impl Quest {
201 #[must_use]
204 pub fn is_red(&self) -> bool {
205 matches!(self.monster_id, 139 | 145 | 148 | 152 | 155 | 157)
206 }
207
208 pub(crate) fn update(&mut self, data: &[i64]) -> Result<(), SFError> {
209 self.monster_id = data.csimget(2, "quest monster id", 0, |a| -a)?;
211 self.location_id = data
212 .cfpget(3, "quest location id", |a| a)?
213 .unwrap_or_default();
214 self.base_length = data.csiget(4, "quest length", 100_000)?;
215 self.base_experience = data.csiget(5, "quest xp", 0)?;
216 self.base_silver = data.csiget(6, "quest silver", 0)?;
217 Ok(())
218 }
219}
220
221#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
223#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
224pub enum CurrentAction {
225 #[default]
227 Idle,
228 CityGuard {
231 hours: u8,
233 busy_until: DateTime<Local>,
235 },
236 Quest {
239 quest_idx: u8,
241 busy_until: DateTime<Local>,
243 },
244 Expedition,
247 Unknown(Option<DateTime<Local>>),
250}
251
252impl CurrentAction {
253 pub(crate) fn parse(
254 id: i64,
255 sec: i64,
256 busy_until: Option<DateTime<Local>>,
257 ) -> Self {
258 let busy_until = busy_until.unwrap_or_default();
262 match id {
263 0 => CurrentAction::Idle,
264 1 => CurrentAction::CityGuard {
265 hours: soft_into(sec, "city guard time", 10),
266 busy_until,
267 },
268 2 => CurrentAction::Quest {
269 quest_idx: soft_into(sec, "quest index", 0),
270 busy_until,
271 },
272 4 => CurrentAction::Expedition,
273 _ => {
274 error!("Unknown action id combination: {id}, {busy_until:?}");
275 CurrentAction::Unknown(Some(busy_until))
276 }
277 }
278 }
279}
280
281#[derive(Debug, Clone, Default, Copy)]
283#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
284pub struct Toilet {
285 pub aura: u32,
287 pub mana_currently: u32,
289 pub mana_total: u32,
291 pub sacrifices_left: u32,
293}
294
295impl Toilet {
296 pub(crate) fn update(
297 &mut self,
298 data: &[i64],
299 server_time: ServerTime,
300 ) -> Result<(), SFError> {
301 self.aura = data.csiget(0, "aura level", 0)?;
302 self.mana_currently = data.csiget(1, "mana now", 0)?;
303 let _unknown_time = data.cstget(2, "mana time", server_time)?;
305 self.mana_total = data.csiget(3, "mana missing", 1000)?;
306 Ok(())
307 }
308}
309
310#[derive(Debug, Clone, Default)]
312#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
313pub struct Expedition {
314 pub items: [Option<ExpeditionThing>; 4],
316
317 pub target_thing: ExpeditionThing,
319 pub target_current: u8,
321 pub target_amount: u8,
323
324 pub current_floor: u8,
326 pub heroism: i32,
328
329 pub(crate) floor_stage: i64,
330
331 pub(crate) rewards: Vec<Reward>,
333 pub(crate) halftime_for_boss_id: i64,
334 pub(crate) boss: ExpeditionBoss,
336 pub(crate) encounters: Vec<ExpeditionEncounter>,
338 pub(crate) busy_until: Option<DateTime<Local>>,
339 pub(crate) busy_since: Option<DateTime<Local>>,
340}
341
342impl Expedition {
343 pub(crate) fn update_encounters(&mut self, data: &[i64]) {
344 if !data.len().is_multiple_of(2) {
345 warn!("weird encounters: {data:?}");
346 }
347 let default_ecp = |ci| {
348 warn!("Unknown encounter: {ci}");
349 ExpeditionThing::Unknown
350 };
351 self.encounters = data
352 .chunks_exact(2)
353 .filter_map(|ci| {
354 let raw = *ci.first()?;
355 let typ = FromPrimitive::from_i64(raw)
356 .unwrap_or_else(|| default_ecp(raw));
357 let heroism = soft_into(*ci.get(1)?, "e heroism", 0);
358 Some(ExpeditionEncounter { typ, heroism })
359 })
360 .collect();
361 }
362
363 #[must_use]
367 pub fn current_stage(&self) -> ExpeditionStage {
368 let cross_roads =
369 || ExpeditionStage::Encounters(self.encounters.clone());
370
371 match self.floor_stage {
372 1 => cross_roads(),
373 2 => ExpeditionStage::Boss(self.boss),
374 3 => ExpeditionStage::Rewards(self.rewards.clone()),
375 4 => match self.busy_until {
376 Some(x) if x > Local::now() => ExpeditionStage::Waiting {
377 busy_until: x,
378 busy_since: self.busy_since.unwrap_or_default(),
379 },
380 _ if self.current_floor == 10 => ExpeditionStage::Finished,
381 _ => cross_roads(),
382 },
383 _ => ExpeditionStage::Unknown,
384 }
385 }
386
387 #[must_use]
389 pub fn is_finished(&self) -> bool {
390 matches!(self.current_stage(), ExpeditionStage::Finished)
391 }
392}
393
394#[derive(Debug, Clone)]
396#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
397pub enum ExpeditionStage {
398 Rewards(Vec<Reward>),
400 Boss(ExpeditionBoss),
402 Encounters(Vec<ExpeditionEncounter>),
404 Waiting {
409 busy_since: DateTime<Local>,
411 busy_until: DateTime<Local>,
413 },
414 Finished,
416 Unknown,
419}
420
421impl Default for ExpeditionStage {
422 fn default() -> Self {
423 ExpeditionStage::Encounters(Vec::new())
424 }
425}
426
427#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
429#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
430pub struct ExpeditionBoss {
431 pub id: i64,
433 pub items: u8,
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
440#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
441pub struct ExpeditionEncounter {
442 pub typ: ExpeditionThing,
444 pub heroism: i32,
447}
448
449#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, Default)]
452#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
453#[allow(missing_docs, clippy::doc_markdown)]
454pub enum ExpeditionThing {
455 #[default]
456 Unknown = 0,
457
458 Dummy1 = 1,
459 Dummy2 = 2,
460 Dummy3 = 3,
461
462 ToiletPaper = 11,
463
464 Bait = 21,
465 Dragon = 22,
467
468 CampFire = 31,
469 Phoenix = 32,
470 BurntCampfire = 33,
472
473 UnicornHorn = 41,
474 Donkey = 42,
475 Rainbow = 43,
476 Unicorn = 44,
478
479 CupCake = 51,
480 Cake = 61,
482
483 SmallHurdle = 71,
484 BigHurdle = 72,
485 WinnersPodium = 73,
487
488 Socks = 81,
489 ClothPile = 82,
490 RevealingCouple = 83,
492
493 SwordInStone = 91,
494 BentSword = 92,
495 BrokenSword = 93,
496
497 Well = 101,
498 Girl = 102,
499 Balloons = 103,
501
502 Prince = 111,
503 RoyalFrog = 112,
505
506 Hand = 121,
507 Feet = 122,
508 Body = 123,
509 Klaus = 124,
511
512 Key = 131,
513 Suitcase = 132,
514
515 FishingRod = 141,
516 FishingBait = 142,
517 Merman = 143,
518
519 Mugs = 151,
520 DraftBeer = 152,
521 Barkeeper = 153,
522
523 Chicken = 161,
524 Tiger = 162,
525 RidingStan = 163,
526
527 Cupid = 171,
529 LovestruckShakes = 172,
530 LoveBirds = 173,
531
532 DummyBounty = 1000,
534 ToiletPaperBounty = 1001,
535 DragonBounty = 1002,
536 BurntCampfireBounty = 1003,
537 UnicornBounty = 1004,
538 WinnerPodiumBounty = 1007,
539 RevealingCoupleBounty = 1008,
540 BrokenSwordBounty = 1009,
541 BaloonBounty = 1010,
542 FrogBounty = 1011,
543 KlausBounty = 1012,
544 MermanBounty = 1014,
546 BarkeeperBounty = 1015,
547 StanBounty = 1016,
548 LoveBirdBounty = 1017,
549}
550
551impl ExpeditionThing {
552 #[must_use]
555 #[allow(clippy::enum_glob_use)]
556 pub fn required_bounty(&self) -> Option<ExpeditionThing> {
557 use ExpeditionThing::*;
558 Some(match self {
559 Dummy1 | Dummy2 | Dummy3 => DummyBounty,
560 ToiletPaper => ToiletPaperBounty,
561 Dragon => DragonBounty,
562 BurntCampfire => BurntCampfireBounty,
563 Unicorn => UnicornBounty,
564 WinnersPodium => WinnerPodiumBounty,
565 RevealingCouple => RevealingCoupleBounty,
566 BrokenSword => BrokenSwordBounty,
567 Balloons => BaloonBounty,
568 RoyalFrog => FrogBounty,
569 Klaus => KlausBounty,
570 Merman => MermanBounty,
571 RidingStan => StanBounty,
572 Barkeeper => BarkeeperBounty,
573 LoveBirds => LoveBirdBounty,
574 _ => return None,
575 })
576 }
577
578 #[must_use]
581 #[allow(clippy::enum_glob_use)]
582 pub fn is_bounty_for(&self) -> Option<&'static [ExpeditionThing]> {
583 use ExpeditionThing::*;
584 Some(match self {
585 DummyBounty => &[Dummy1, Dummy2, Dummy3],
586 ToiletPaperBounty => &[ToiletPaper],
587 DragonBounty => &[Dragon],
588 BurntCampfireBounty => &[BurntCampfire],
589 UnicornBounty => &[Unicorn],
590 WinnerPodiumBounty => &[WinnersPodium],
591 RevealingCoupleBounty => &[RevealingCouple],
592 BrokenSwordBounty => &[BrokenSword],
593 BaloonBounty => &[Balloons],
594 FrogBounty => &[RoyalFrog],
595 KlausBounty => &[Klaus],
596 MermanBounty => &[Merman],
597 StanBounty => &[RidingStan],
598 BarkeeperBounty => &[Barkeeper],
599 LoveBirdBounty => &[LoveBirds],
600 _ => return None,
601 })
602 }
603}
604
605#[derive(Debug, Clone, Copy, PartialEq, Eq)]
607#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
608pub struct AvailableExpedition {
609 pub target: ExpeditionThing,
611 pub thirst_for_adventure_sec: u32,
614 pub location_1: Location,
617 pub location_2: Location,
620 pub special: Option<ExpeditionSpecial>,
623}
624
625#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive)]
628#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
629#[allow(missing_docs)]
630pub enum ExpeditionSpecial {
631 Egg = 1,
633 DailyTask,
635}
636
637#[derive(Debug, Clone, Copy, PartialEq, Eq)]
640#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
641#[allow(missing_docs)]
642pub enum GambleResult {
643 SilverChange(i64),
644 MushroomChange(i32),
645}