1use chrono::{DateTime, Local};
2use log::{error, warn};
3use num_derive::FromPrimitive;
4use num_traits::FromPrimitive;
5
6use super::{
7 ArrSkip, CCGet, CFPGet, CGet, CSTGet, ExpeditionSetting, SFError,
8 ServerTime, items::Item,
9};
10use crate::{
11 command::{DiceReward, DiceType},
12 gamestate::rewards::Reward,
13 misc::soft_into,
14};
15
16#[derive(Debug, Clone, Default)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub struct Tavern {
20 pub quests: [Quest; 3],
22 #[doc(alias = "alu")]
24 pub thirst_for_adventure_sec: u32,
25 pub mushroom_skip_allowed: bool,
27 pub beer_drunk: u8,
29 pub quicksand_glasses: u32,
31 pub current_action: CurrentAction,
33 pub guard_wage: u64,
35 pub toilet: Option<Toilet>,
37 pub dice_game: DiceGame,
39 pub expeditions: ExpeditionsEvent,
41 pub questing_preference: ExpeditionSetting,
44 pub gamble_result: Option<GambleResult>,
46 pub beer_max: u8,
48}
49
50#[derive(Debug, Clone, Default)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53pub struct ExpeditionsEvent {
54 pub start: Option<DateTime<Local>>,
56 pub end: Option<DateTime<Local>>,
58 pub available: Vec<AvailableExpedition>,
60 pub(crate) active: Option<Expedition>,
63}
64
65impl ExpeditionsEvent {
66 #[must_use]
69 pub fn is_event_ongoing(&self) -> bool {
70 let now = Local::now();
71 matches!((self.start, self.end), (Some(start), Some(end)) if end > now && start < now)
72 }
73
74 #[must_use]
78 pub fn active(&self) -> Option<&Expedition> {
79 self.active.as_ref().filter(|a| !a.is_finished())
80 }
81
82 #[must_use]
86 pub fn active_mut(&mut self) -> Option<&mut Expedition> {
87 self.active.as_mut().filter(|a| !a.is_finished())
88 }
89}
90
91#[derive(Debug, Clone, Default)]
93#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
94pub struct DiceGame {
95 pub remaining: u8,
97 pub next_free: Option<DateTime<Local>>,
99 pub current_dice: Vec<DiceType>,
102 pub reward: Option<DiceReward>,
104}
105
106#[derive(Debug, Clone)]
110#[allow(missing_docs)]
111pub enum AvailableTasks<'a> {
112 Quests(&'a [Quest; 3]),
113 Expeditions(&'a [AvailableExpedition]),
114}
115
116impl Tavern {
117 #[must_use]
123 pub fn is_idle(&self) -> bool {
124 match self.current_action {
125 CurrentAction::Idle => true,
126 CurrentAction::Expedition => self.expeditions.active.is_none(),
127 _ => false,
128 }
129 }
130
131 #[must_use]
136 pub fn available_tasks(&self) -> AvailableTasks<'_> {
137 if self.questing_preference == ExpeditionSetting::PreferExpeditions
138 && self.expeditions.is_event_ongoing()
139 {
140 AvailableTasks::Expeditions(&self.expeditions.available)
141 } else {
142 AvailableTasks::Quests(&self.quests)
143 }
144 }
145
146 #[must_use]
149 pub fn can_change_questing_preference(&self) -> bool {
150 self.thirst_for_adventure_sec == 6000 && self.beer_drunk == 0
151 }
152
153 pub(crate) fn update(
154 &mut self,
155 data: &[i64],
156 server_time: ServerTime,
157 ) -> Result<(), SFError> {
158 self.current_action = CurrentAction::parse(
159 data.cget(45, "action id")? & 0xFF,
160 data.cget(46, "action sec")? & 0xFF,
161 data.cstget(47, "current action time", server_time)?,
162 );
163 self.thirst_for_adventure_sec = data.csiget(456, "remaining ALU", 0)?;
164 self.beer_drunk = data.csiget(457, "beer drunk count", 0)?;
165 self.beer_max = data.csiget(13, "beer total", 0)?;
166
167 for (qidx, quest) in self.quests.iter_mut().enumerate() {
168 let quest_start = data.skip(235 + qidx, "tavern quest")?;
169 quest.update(quest_start)?;
170 }
171 Ok(())
172 }
173}
174
175#[derive(Debug, Default, Clone, PartialEq, Eq)]
177#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
178pub struct Quest {
179 pub base_length: u32,
181 pub base_silver: u32,
183 pub base_experience: u32,
185 pub item: Option<Item>,
187 pub location_id: Location,
189 pub monster_id: u16,
191}
192
193#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, FromPrimitive, Hash)]
195#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
196#[allow(missing_docs)]
197pub enum Location {
198 #[default]
199 SprawlingJungle = 1,
200 SkullIsland,
201 EvernightForest,
202 StumbleSteppe,
203 ShadowrockMountain,
204 SplitCanyon,
205 BlackWaterSwamp,
206 FloodedCaldwell,
207 TuskMountain,
208 MoldyForest,
209 Nevermoor,
210 BustedLands,
211 Erogenion,
212 Magmaron,
213 SunburnDesert,
214 Gnarogrim,
215 Northrunt,
216 BlackForest,
217 Maerwynn,
218 PlainsOfOzKorr,
219 RottenLands,
220}
221
222impl Quest {
223 #[must_use]
226 pub fn is_red(&self) -> bool {
227 matches!(self.monster_id, 139 | 145 | 148 | 152 | 155 | 157)
228 }
229
230 pub(crate) fn update(&mut self, data: &[i64]) -> Result<(), SFError> {
231 self.base_length = data.csiget(6, "quest length", 100_000)?;
232 self.base_silver = data.csiget(48, "quest silver", 0)?;
233 self.base_experience = data.csiget(45, "quest xp", 0)?;
234 self.location_id = data
235 .cfpget(3, "quest location id", |a| a)?
236 .unwrap_or_default();
237 self.monster_id = data.csimget(0, "quest monster id", 0, |a| -a)?;
238 Ok(())
239 }
240}
241
242#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
244#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
245pub enum CurrentAction {
246 #[default]
248 Idle,
249 CityGuard {
252 hours: u8,
254 busy_until: DateTime<Local>,
256 },
257 Quest {
260 quest_idx: u8,
262 busy_until: DateTime<Local>,
264 },
265 Expedition,
268 Unknown(Option<DateTime<Local>>),
271}
272
273impl CurrentAction {
274 pub(crate) fn parse(
275 id: i64,
276 sec: i64,
277 busy: Option<DateTime<Local>>,
278 ) -> Self {
279 match (id, busy) {
280 (0, None) => CurrentAction::Idle,
281 (1, Some(busy_until)) => CurrentAction::CityGuard {
282 hours: soft_into(sec, "city guard time", 10),
283 busy_until,
284 },
285 (2, Some(busy_until)) => CurrentAction::Quest {
286 quest_idx: soft_into(sec, "quest index", 0),
287 busy_until,
288 },
289 (4, None) => CurrentAction::Expedition,
290 _ => {
291 error!("Unknown action id combination: {id}, {busy:?}");
292 CurrentAction::Unknown(busy)
293 }
294 }
295 }
296}
297
298#[derive(Debug, Clone, Default, Copy)]
300#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
301pub struct Toilet {
302 #[deprecated(note = "You should use sacrifices_left instead")]
304 pub used: bool,
305 pub aura: u32,
307 pub mana_currently: u32,
309 pub mana_total: u32,
311 pub sacrifices_left: u32,
313}
314
315impl Toilet {
316 pub(crate) fn update(&mut self, data: &[i64]) -> Result<(), SFError> {
317 self.aura = data.csiget(491, "aura level", 0)?;
318 self.mana_currently = data.csiget(492, "mana now", 0)?;
319 self.mana_total = data.csiget(515, "mana missing", 1000)?;
320 Ok(())
321 }
322}
323
324#[derive(Debug, Clone, Default)]
326#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
327pub struct Expedition {
328 pub items: [Option<ExpeditionThing>; 4],
330
331 pub target_thing: ExpeditionThing,
333 pub target_current: u8,
335 pub target_amount: u8,
337
338 pub current_floor: u8,
340 pub heroism: i32,
342
343 pub(crate) adjusted_bounty_heroism: bool,
344
345 pub(crate) floor_stage: i64,
346
347 pub(crate) rewards: Vec<Reward>,
349 pub(crate) halftime_for_boss_id: i64,
350 pub(crate) boss: ExpeditionBoss,
352 pub(crate) encounters: Vec<ExpeditionEncounter>,
354 pub(crate) busy_until: Option<DateTime<Local>>,
355}
356
357impl Expedition {
358 pub(crate) fn adjust_bounty_heroism(&mut self) {
359 if self.adjusted_bounty_heroism {
360 return;
361 }
362
363 for ExpeditionEncounter { typ, heroism } in &mut self.encounters {
364 if let Some(possible_bounty) = typ.required_bounty()
365 && self.items.iter().flatten().any(|a| a == &possible_bounty)
366 {
367 *heroism += 10;
368 }
369 }
370 self.adjusted_bounty_heroism = true;
371 }
372
373 pub(crate) fn update_encounters(&mut self, data: &[i64]) {
374 if !data.len().is_multiple_of(2) {
375 warn!("weird encounters: {data:?}");
376 }
377 let default_ecp = |ci| {
378 warn!("Unknown encounter: {ci}");
379 ExpeditionThing::Unknown
380 };
381 self.encounters = data
382 .chunks_exact(2)
383 .filter_map(|ci| {
384 let raw = *ci.first()?;
385 let typ = FromPrimitive::from_i64(raw)
386 .unwrap_or_else(|| default_ecp(raw));
387 let heroism = soft_into(*ci.get(1)?, "e heroism", 0);
388 Some(ExpeditionEncounter { typ, heroism })
389 })
390 .collect();
391 }
392
393 #[must_use]
397 pub fn current_stage(&self) -> ExpeditionStage {
398 let cross_roads =
399 || ExpeditionStage::Encounters(self.encounters.clone());
400
401 match self.floor_stage {
402 1 => cross_roads(),
403 2 => ExpeditionStage::Boss(self.boss),
404 3 => ExpeditionStage::Rewards(self.rewards.clone()),
405 4 => match self.busy_until {
406 Some(x) if x > Local::now() => ExpeditionStage::Waiting(x),
407 _ if self.current_floor == 10 => ExpeditionStage::Finished,
408 _ => cross_roads(),
409 },
410 _ => ExpeditionStage::Unknown,
411 }
412 }
413
414 #[must_use]
416 pub fn is_finished(&self) -> bool {
417 matches!(self.current_stage(), ExpeditionStage::Finished)
418 }
419}
420
421#[derive(Debug, Clone)]
423#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
424pub enum ExpeditionStage {
425 Rewards(Vec<Reward>),
427 Boss(ExpeditionBoss),
429 Encounters(Vec<ExpeditionEncounter>),
431 Waiting(DateTime<Local>),
436 Finished,
438 Unknown,
441}
442
443impl Default for ExpeditionStage {
444 fn default() -> Self {
445 ExpeditionStage::Encounters(Vec::new())
446 }
447}
448
449#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
451#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
452pub struct ExpeditionBoss {
453 pub id: i64,
455 pub items: u8,
457}
458
459#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
462#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
463pub struct ExpeditionEncounter {
464 pub typ: ExpeditionThing,
466 pub heroism: i32,
469}
470
471#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, Default)]
474#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
475#[allow(missing_docs, clippy::doc_markdown)]
476pub enum ExpeditionThing {
477 #[default]
478 Unknown = 0,
479
480 Dummy1 = 1,
481 Dummy2 = 2,
482 Dumy3 = 3,
483
484 ToiletPaper = 11,
485
486 Bait = 21,
487 Dragon = 22,
489
490 CampFire = 31,
491 Phoenix = 32,
492 BurntCampfire = 33,
494
495 UnicornHorn = 41,
496 Donkey = 42,
497 Rainbow = 43,
498 Unicorn = 44,
500
501 CupCake = 51,
502 Cake = 61,
504
505 SmallHurdle = 71,
506 BigHurdle = 72,
507 WinnersPodium = 73,
509
510 Socks = 81,
511 ClothPile = 82,
512 RevealingCouple = 83,
514
515 SwordInStone = 91,
516 BentSword = 92,
517 BrokenSword = 93,
518
519 Well = 101,
520 Girl = 102,
521 Balloons = 103,
523
524 Prince = 111,
525 RoyalFrog = 112,
527
528 Hand = 121,
529 Feet = 122,
530 Body = 123,
531 Klaus = 124,
533
534 Key = 131,
535 Suitcase = 132,
536
537 DummyBounty = 1000,
539 ToiletPaperBounty = 1001,
540 DragonBounty = 1002,
541 BurntCampfireBounty = 1003,
542 UnicornBounty = 1004,
543 WinnerPodiumBounty = 1007,
544 RevealingCoupleBounty = 1008,
545 BrokenSwordBounty = 1009,
546 BaloonBounty = 1010,
547 FrogBounty = 1011,
548 KlausBounty = 1012,
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 | Dumy3 => 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 _ => return None,
571 })
572 }
573
574 #[must_use]
577 #[allow(clippy::enum_glob_use)]
578 pub fn is_bounty_for(&self) -> Option<&'static [ExpeditionThing]> {
579 use ExpeditionThing::*;
580 Some(match self {
581 DummyBounty => &[Dummy1, Dummy2, Dumy3],
582 ToiletPaperBounty => &[ToiletPaper],
583 DragonBounty => &[Dragon],
584 BurntCampfireBounty => &[BurntCampfire],
585 UnicornBounty => &[Unicorn],
586 WinnerPodiumBounty => &[WinnersPodium],
587 RevealingCoupleBounty => &[RevealingCouple],
588 BrokenSwordBounty => &[BrokenSword],
589 BaloonBounty => &[Balloons],
590 FrogBounty => &[RoyalFrog],
591 KlausBounty => &[Klaus],
592 _ => return None,
593 })
594 }
595}
596
597#[derive(Debug, Clone, Copy, PartialEq, Eq)]
599#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
600pub struct AvailableExpedition {
601 pub target: ExpeditionThing,
603 pub thirst_for_adventure_sec: u32,
606 pub location_1: Location,
609 pub location_2: Location,
612}
613
614#[derive(Debug, Clone, Copy, PartialEq, Eq)]
617#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
618#[allow(missing_docs)]
619pub enum GambleResult {
620 SilverChange(i64),
621 MushroomChange(i32),
622}