use std::collections::HashSet;
use chrono::{DateTime, Local};
use log::warn;
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use strum::EnumIter;
use super::{
ArrSkip, CCGet, CGet, IdleBuildingType, LightDungeon, Mount, ShopType,
character::Class, items::*, tavern::Location, unlockables::HabitatType,
};
use crate::{command::AttributeType, error::SFError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum WheelRewardType {
Mushrooms,
Stone,
StoneXL,
Wood,
WoodXL,
Experience,
ExperienceXL,
Silver,
SilverXL,
Arcane,
Souls,
Item,
PetItem(PetItem),
Unknown,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WheelReward {
pub typ: WheelRewardType,
pub amount: i64,
}
impl WheelReward {
pub(crate) fn parse(
data: &[i64],
upgraded: bool,
) -> Result<WheelReward, SFError> {
let raw_typ = data.cget(0, "wheel reward typ")?;
let mut amount = data.cget(1, "wheel reward amount")?;
let typ = match raw_typ {
0 => WheelRewardType::Mushrooms,
1 => {
if upgraded {
WheelRewardType::Arcane
} else {
WheelRewardType::Wood
}
}
2 => WheelRewardType::ExperienceXL,
3 => {
if upgraded {
let res = WheelRewardType::PetItem(
PetItem::parse(amount).ok_or_else(|| {
SFError::ParsingError(
"pet wheel reward type",
amount.to_string(),
)
})?,
);
amount = 1;
res
} else {
WheelRewardType::Stone
}
}
4 => WheelRewardType::SilverXL,
5 => {
amount = 1;
WheelRewardType::Item
}
6 => WheelRewardType::WoodXL,
7 => WheelRewardType::Experience,
8 => WheelRewardType::StoneXL,
9 => {
if upgraded {
WheelRewardType::Souls
} else {
WheelRewardType::Silver
}
}
x => {
warn!("unknown wheel reward type: {x}");
WheelRewardType::Unknown
}
};
Ok(WheelReward { typ, amount })
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CalendarReward {
pub typ: CalendarRewardType,
pub amount: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[allow(missing_docs)]
pub enum CalendarRewardType {
Silver,
Mushrooms,
Experience,
Wood,
Stone,
Souls,
Arcane,
Runes,
Item,
Attribute(AttributeType),
Fruit(HabitatType),
Level,
Potion(PotionType),
TenQuicksandGlasses,
LevelUp,
Unknown,
}
impl CalendarReward {
pub(crate) fn parse(data: &[i64]) -> Result<CalendarReward, SFError> {
let amount = data.cget(1, "c reward amount")?;
let typ = data.cget(0, "c reward typ")?;
let typ = match typ {
1 => CalendarRewardType::Silver,
2 => CalendarRewardType::Mushrooms,
3 => CalendarRewardType::Experience,
4 => CalendarRewardType::Wood,
5 => CalendarRewardType::Stone,
6 => CalendarRewardType::Souls,
7 => CalendarRewardType::Arcane,
8 => CalendarRewardType::Runes,
10 => CalendarRewardType::Item,
11 => CalendarRewardType::Attribute(AttributeType::Strength),
12 => CalendarRewardType::Attribute(AttributeType::Dexterity),
13 => CalendarRewardType::Attribute(AttributeType::Intelligence),
14 => CalendarRewardType::Attribute(AttributeType::Constitution),
15 => CalendarRewardType::Attribute(AttributeType::Luck),
x @ 16..=20 => {
if let Some(typ) = HabitatType::from_typ_id(x - 15) {
CalendarRewardType::Fruit(typ)
} else {
warn!("unknown pet class in c rewards");
CalendarRewardType::Unknown
}
}
21 => CalendarRewardType::LevelUp,
22 => CalendarRewardType::Potion(PotionType::EternalLife),
23 => CalendarRewardType::TenQuicksandGlasses,
24 => CalendarRewardType::Potion(PotionType::Strength),
25 => CalendarRewardType::Potion(PotionType::Dexterity),
26 => CalendarRewardType::Potion(PotionType::Intelligence),
27 => CalendarRewardType::Potion(PotionType::Constitution),
28 => CalendarRewardType::Potion(PotionType::Luck),
x => {
warn!("Unknown calendar reward: {x}");
CalendarRewardType::Unknown
}
};
Ok(CalendarReward { typ, amount })
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TimedSpecials {
pub events: Events,
pub tasks: Tasks,
pub calendar: Calendar,
pub wheel: Wheel,
pub advent_calendar: Option<Reward>,
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Events {
pub active: HashSet<Event>,
pub ends: Option<DateTime<Local>>,
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[doc(alias = "DailyLoginBonus")]
pub struct Calendar {
pub collected: usize,
pub rewards: Vec<CalendarReward>,
pub next_possible: Option<DateTime<Local>>,
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Tasks {
pub daily: DailyTasks,
pub event: EventTasks,
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DailyTasks {
pub tasks: Vec<Task>,
pub rewards: [RewardChest; 3],
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EventTasks {
pub theme: EventTaskTheme,
pub start: Option<DateTime<Local>>,
pub end: Option<DateTime<Local>>,
pub tasks: Vec<Task>,
pub rewards: [RewardChest; 3],
}
macro_rules! impl_tasks {
($t:ty) => {
impl $t {
#[must_use]
pub fn completed(&self) -> usize {
self.tasks.iter().filter(|a| a.is_completed()).count()
}
#[must_use]
pub fn earned_points(&self) -> u32 {
self.tasks
.iter()
.filter(|a| a.is_completed())
.map(|a| a.point_reward)
.sum()
}
#[must_use]
pub fn total_points(&self) -> u32 {
self.tasks.iter().map(|a| a.point_reward).sum()
}
#[must_use]
pub fn get_available(&self, task_type: TaskType) -> Option<&Task> {
self.tasks
.iter()
.find(|task| task.typ == task_type && !task.is_completed())
}
#[must_use]
pub fn get_uncompleted(&self) -> Vec<&Task> {
self.tasks
.iter()
.filter(|task| !task.is_completed())
.collect()
}
#[must_use]
pub fn can_open_chest(&self, index: usize) -> bool {
let Some(chest) = self.rewards.get(index) else {
return false;
};
if chest.opened {
return false;
}
self.earned_points() >= chest.required_points
}
}
};
}
impl_tasks!(DailyTasks);
impl_tasks!(EventTasks);
impl Task {
#[must_use]
pub fn is_completed(&self) -> bool {
self.current >= self.target
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Wheel {
pub lucky_coins: u32,
pub spins_today: u8,
pub next_free_spin: Option<DateTime<Local>>,
pub result: Option<WheelReward>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum EventTaskTheme {
Gambler = 2,
RankClimber = 3,
ShoppingSpree = 4,
TimeSkipper = 5,
RuffianReset = 6,
PartTimeNudist = 7,
Scrimper = 8,
Scholar = 9,
Maximizer = 10,
UnderworldFigure = 11,
EggHunt = 12,
SummerCollectifun = 13,
Walpurgis = 14,
PetTrainer = 15,
FortressMaster = 16,
LegendaryDungeon = 17,
Hellevator = 18,
#[default]
Unknown = 245,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum TaskType {
AddSocketToItem,
BlacksmithDismantle,
BuyHourGlasses,
BuyOfferFromArenaManager,
ClaimSoulsFromExtractor,
CollectGoldFromPit,
ConsumeThirstForAdventure,
ConsumeThirstFromUnderworld,
DefeatGambler,
DrinkBeer,
EarnMoneyCityGuard,
EarnMoneyFromHoFFights,
EarnMoneySellingItems,
EnterDemonPortal,
FeedPets,
FightGuildHydra,
FightGuildPortal,
FightInDungeons,
FightInPetHabitat,
FightMonsterInLegendaryDungeon,
FightOtherPets,
FillMushroomsInAdventuromatic,
FindGemInFortress,
GainArcaneFromDismantle,
GainEpic,
GainHonorExpeditions,
GainHonorFortress,
GainHonorInArena,
GainHonorInHoF,
GainLegendaryFromLegendaryDungeon,
GainMetalFromDismantle,
GainSilver,
GainSilverFromFightsInHoF,
GainXP,
GainXpFromAcademy,
GainXpFromAdventuromatic,
GainXpFromArenaFights,
GainXpFromQuests,
GetLuckyCoinsFromFlyingTube,
GuildReadyFight,
LureHeroesIntoUnderworld,
PlayGameOfDice,
RequestNewGoods,
SacrificeRunes,
SkipGameOfDiceWait,
SkipQuest,
SpendGoldInShop,
SpendGoldOnUpgrades,
SpinWheelOfFortune,
ThrowEpicInToilet,
ThrowItemInCauldron,
ThrowItemInToilet,
TravelTo(Location),
Upgrade(AttributeType),
UpgradeAnyAttribute,
UpgradeArenaManager,
UpgradeItemAttributes,
WinFightsPlayerPet,
WinFightsAgainst(Class),
WinFightsBackToBack,
WinFightsBareHands,
WinFightsInArena,
WinFightsInHoF,
WinFightsNoChestplate,
WinFightsNoEpicsLegendaries,
WinFightsNoGear,
LeaseMount,
DefeatMonstersLightDungeon(LightDungeon),
BuyWeaponInWeaponsShop,
FightHigherRankedPlayer,
AddFriend,
ClaimNewCustomerPack,
JoinOrCreateGuild,
UpgradeAnyGuildSkill,
CityGuardHours,
DrinkPotion(PotionType),
BuyFromShop(ShopType),
FindFruitsOnExpedition,
BrewPotions,
CollectWood,
CollectStone,
CommandFortressBattle,
FightHellevator,
BuyHellevatorTreats,
DefeatHellevatorFloors,
EnterLegendaryDungeon,
OpenLegendaryDungeonCrateChests,
FeedPetType(HabitatType),
SpendCardsHellevator,
OpenAdventCalendar,
UpgradeArenaManagerBuilding(IdleBuildingType),
EarnMoneyFromExpeditions,
Unknown,
}
impl TaskType {
pub(crate) fn parse(num: i64) -> TaskType {
match num {
1 => TaskType::DrinkBeer,
2 => TaskType::ConsumeThirstForAdventure,
3 => TaskType::WinFightsInArena,
4 => TaskType::SpinWheelOfFortune,
5 => TaskType::FightGuildHydra,
6 => TaskType::FightGuildPortal,
7 => TaskType::FeedPets,
8 => TaskType::FightOtherPets,
9 => TaskType::BlacksmithDismantle,
10 => TaskType::ThrowItemInToilet,
11 => TaskType::PlayGameOfDice,
12 => TaskType::LureHeroesIntoUnderworld,
13 => TaskType::EnterDemonPortal,
14 => TaskType::DefeatGambler,
15 => TaskType::Upgrade(AttributeType::Strength),
16 => TaskType::Upgrade(AttributeType::Dexterity),
17 => TaskType::Upgrade(AttributeType::Intelligence),
18 => TaskType::ConsumeThirstFromUnderworld,
19 => TaskType::GuildReadyFight,
20 => TaskType::FindGemInFortress,
21 => TaskType::ThrowItemInCauldron,
22 => TaskType::FightInPetHabitat,
23 => TaskType::UpgradeArenaManager,
24 => TaskType::SacrificeRunes,
25..=45 => {
let Some(location) = FromPrimitive::from_i64(num - 24) else {
return TaskType::Unknown;
};
TaskType::TravelTo(location)
}
46 => TaskType::ThrowEpicInToilet,
47 => TaskType::BuyOfferFromArenaManager,
48 => TaskType::WinFightsAgainst(Class::Warrior),
49 => TaskType::WinFightsAgainst(Class::Mage),
50 => TaskType::WinFightsAgainst(Class::Scout),
51 => TaskType::WinFightsAgainst(Class::Assassin),
52 => TaskType::WinFightsAgainst(Class::Druid),
53 => TaskType::WinFightsAgainst(Class::Bard),
54 => TaskType::WinFightsAgainst(Class::BattleMage),
55 => TaskType::WinFightsAgainst(Class::Berserker),
56 => TaskType::WinFightsAgainst(Class::DemonHunter),
57 => TaskType::WinFightsBareHands,
58 => TaskType::WinFightsPlayerPet,
59 => TaskType::GetLuckyCoinsFromFlyingTube,
60 => TaskType::Upgrade(AttributeType::Luck),
61 => TaskType::GainHonorInArena,
62 => TaskType::GainHonorInHoF,
63 => TaskType::GainHonorFortress,
64 => TaskType::GainHonorExpeditions,
65 => TaskType::SpendGoldInShop,
66 => TaskType::SpendGoldOnUpgrades,
67 => TaskType::RequestNewGoods,
68 => TaskType::BuyHourGlasses,
69 => TaskType::SkipQuest,
70 => TaskType::SkipGameOfDiceWait,
71 => TaskType::WinFightsInHoF,
72 => TaskType::WinFightsBackToBack,
73 => TaskType::SpendCardsHellevator,
74 => TaskType::GainSilverFromFightsInHoF,
75 => TaskType::WinFightsNoChestplate,
76 => TaskType::WinFightsNoGear,
77 => TaskType::WinFightsNoEpicsLegendaries,
78 => TaskType::EarnMoneyCityGuard,
79 => TaskType::EarnMoneyFromHoFFights,
80 => TaskType::EarnMoneySellingItems,
81 => TaskType::CollectGoldFromPit,
82 => TaskType::GainXpFromQuests,
83 => TaskType::GainXpFromAcademy,
84 => TaskType::GainXpFromArenaFights,
85 => TaskType::GainXpFromAdventuromatic,
86 => TaskType::GainArcaneFromDismantle,
87 => TaskType::GainMetalFromDismantle,
88 => TaskType::UpgradeItemAttributes,
89 => TaskType::AddSocketToItem,
90 => TaskType::ClaimSoulsFromExtractor,
91 => TaskType::FillMushroomsInAdventuromatic,
92 => TaskType::WinFightsAgainst(Class::Necromancer),
93 => TaskType::GainLegendaryFromLegendaryDungeon,
94 => TaskType::FightMonsterInLegendaryDungeon,
95 => TaskType::FightInDungeons,
96 => TaskType::UpgradeAnyAttribute,
97 => TaskType::GainSilver,
98 => TaskType::GainXP,
99 => TaskType::GainEpic,
100 => TaskType::BuyFromShop(ShopType::Magic),
101 => TaskType::BuyFromShop(ShopType::Weapon),
102 => TaskType::FindFruitsOnExpedition,
103 => TaskType::BrewPotions,
104 => TaskType::CollectWood,
105 => TaskType::CollectStone,
106 => TaskType::CommandFortressBattle,
107 => TaskType::FightHellevator,
108 => TaskType::BuyHellevatorTreats,
109 => TaskType::DefeatHellevatorFloors,
110 => TaskType::EnterLegendaryDungeon,
111 => TaskType::OpenLegendaryDungeonCrateChests,
112 => TaskType::FeedPetType(HabitatType::Shadow),
113 => TaskType::FeedPetType(HabitatType::Light),
114 => TaskType::FeedPetType(HabitatType::Earth),
115 => TaskType::FeedPetType(HabitatType::Fire),
116 => TaskType::FeedPetType(HabitatType::Water),
117 => {
TaskType::DefeatMonstersLightDungeon(LightDungeon::TrainingCamp)
}
118 => TaskType::ClaimNewCustomerPack,
119 => TaskType::JoinOrCreateGuild,
120 => TaskType::UpgradeAnyGuildSkill,
121 => TaskType::AddFriend,
122 => TaskType::DrinkPotion(PotionType::Constitution),
123 => TaskType::DrinkPotion(PotionType::Strength),
124 => TaskType::DrinkPotion(PotionType::Dexterity),
125 => TaskType::DrinkPotion(PotionType::Intelligence),
126 => TaskType::DrinkPotion(PotionType::EternalLife),
127 => TaskType::LeaseMount,
128 => TaskType::FightHigherRankedPlayer,
129 => TaskType::CityGuardHours,
130 => TaskType::BuyWeaponInWeaponsShop,
131 => TaskType::Upgrade(AttributeType::Constitution),
132 => TaskType::WinFightsAgainst(Class::Paladin),
133 => {
TaskType::UpgradeArenaManagerBuilding(IdleBuildingType::Seat)
}
134 => TaskType::OpenAdventCalendar,
135 => TaskType::EarnMoneyFromExpeditions,
136 => TaskType::WinFightsAgainst(Class::PlagueDoctor),
..=0 | 137.. => TaskType::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Task {
pub typ: TaskType,
pub current: u64,
pub target: u64,
pub point_reward: u32,
}
impl Task {
pub(crate) fn parse(data: &[i64]) -> Result<Task, SFError> {
let raw_typ = data.cget(0, "task typ")?;
let typ = TaskType::parse(raw_typ);
if typ == TaskType::Unknown {
warn!("Unknown task: {data:?} {raw_typ}");
}
Ok(Task {
typ,
current: data.csiget(1, "current ti", 0)?,
target: data.csiget(2, "target ti", u64::MAX)?,
point_reward: data.csiget(3, "reward ti", 0)?,
})
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RewardChest {
pub opened: bool,
pub required_points: u32,
pub rewards: Vec<Reward>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Reward {
pub typ: RewardType,
pub amount: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RewardType {
HellevatorPoints,
HellevatorCards,
Mushrooms,
Silver,
LuckyCoins,
Wood,
Stone,
Arcane,
Metal,
Souls,
Fruit(HabitatType),
LegendaryGem,
GoldFidget,
SilverFidget,
BronzeFidget,
Gem,
FruitBasket,
XP,
Egg,
QuicksandGlass,
Honor,
Beer,
Frame,
Mount(Mount),
Unknown,
}
impl RewardType {
#[must_use]
pub(crate) fn parse(val: i64) -> RewardType {
match val {
1 => RewardType::HellevatorPoints,
2 => RewardType::HellevatorCards,
3 => RewardType::Mushrooms,
4 => RewardType::Silver,
5 => RewardType::LuckyCoins,
6 => RewardType::Wood,
7 => RewardType::Stone,
8 => RewardType::Arcane,
9 => RewardType::Metal,
10 => RewardType::Souls,
11 => RewardType::Fruit(HabitatType::Shadow),
12 => RewardType::Fruit(HabitatType::Light),
13 => RewardType::Fruit(HabitatType::Earth),
14 => RewardType::Fruit(HabitatType::Fire),
15 => RewardType::Fruit(HabitatType::Water),
16 => RewardType::LegendaryGem,
17 => RewardType::GoldFidget,
18 => RewardType::SilverFidget,
19 => RewardType::BronzeFidget,
20..=22 => RewardType::Gem,
23 => RewardType::FruitBasket,
24 => RewardType::XP,
25 => RewardType::Egg,
26 => RewardType::QuicksandGlass,
27 => RewardType::Honor,
28 => RewardType::Beer,
29 => RewardType::Frame,
30 => RewardType::Mount(Mount::Cow),
31 => RewardType::Mount(Mount::Horse),
32 => RewardType::Mount(Mount::Tiger),
33 => RewardType::Mount(Mount::Dragon),
x => {
warn!("Unknown reward type: {x}");
RewardType::Unknown
}
}
}
}
impl Reward {
pub(crate) fn parse(data: &[i64]) -> Result<Reward, SFError> {
Ok(Reward {
typ: RewardType::parse(data.cget(0, "reward typ")?),
amount: data.csiget(1, "reward amount", 0)?,
})
}
}
impl RewardChest {
pub(crate) fn parse(data: &[i64]) -> Result<RewardChest, SFError> {
let opened = data.cget(0, "rchest opened")? != 0;
let required_points = data.ciget(1, "reward chest required points")?;
let reward_count: usize = data.ciget(2, "reward chest count")?;
let mut rewards = Vec::new();
for pos in 0..reward_count {
let data = data.skip(3 + pos * 2, "rchest rewards")?;
rewards.push(Reward::parse(data)?);
}
Ok(RewardChest {
opened,
required_points,
rewards,
})
}
}
#[derive(Debug, Clone, Copy, FromPrimitive, PartialEq, Eq, Hash, EnumIter)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[allow(missing_docs)]
pub enum Event {
ExceptionalXPEvent = 0,
GloriousGoldGalore,
TidyToiletTime,
AssemblyOfAwesomeAnimals,
FantasticFortressFestivity,
DaysOfDoomedSouls,
WitchesDance,
SandsOfTimeSpecial,
ForgeFrenzyFestival,
EpicShoppingSpreeExtravaganza,
EpicQuestExtravaganza,
EpicGoodLuckExtravaganza,
OneBeerTwoBeerFreeBeer,
PieceworkParty,
LuckyDay,
CrazyMushroomHarvest,
HolidaySale,
ValentinesBlessing,
BlackGemRush,
RumbleForRiches,
}
pub(crate) fn parse_rewards(vals: &[i64]) -> [RewardChest; 3] {
let mut start = 0;
core::array::from_fn(|_| -> Result<RewardChest, SFError> {
let vals = vals.skip(start, "multi reward chest")?;
let chest = RewardChest::parse(vals)?;
let consumed = 3 + chest.rewards.len() * 2;
start += consumed;
Ok(chest)
})
.map(|res| match res {
Ok(res) => res,
Err(err) => {
warn!("Bad task rewards: {err}");
RewardChest::default()
}
})
}