diff --git a/Cargo.lock b/Cargo.lock index 5a8e429..aea1abb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "squad-quest" -version = "0.8.0" +version = "0.9.0" dependencies = [ "serde", "toml", @@ -1607,7 +1607,7 @@ dependencies = [ [[package]] name = "squad-quest-cli" -version = "0.8.0" +version = "0.9.0" dependencies = [ "chrono", "clap", @@ -1618,7 +1618,7 @@ dependencies = [ [[package]] name = "squad-quest-discord" -version = "0.8.0" +version = "0.9.0" dependencies = [ "clap", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index 8e47446..bb709d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["cli", "discord"] [workspace.package] -version = "0.8.0" +version = "0.9.0" edition = "2024" repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest" license = "MIT" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 1027d59..2006986 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -9,5 +9,5 @@ license.workspace = true chrono = "0.4.42" clap = { version = "4.5.53", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] } -squad-quest = { version = "0.8.0", path = ".." } +squad-quest = { version = "0.9.0", path = ".." } toml = "0.9.8" diff --git a/discord/Cargo.toml b/discord/Cargo.toml index 5b754a9..399606a 100644 --- a/discord/Cargo.toml +++ b/discord/Cargo.toml @@ -10,6 +10,6 @@ clap = { version = "4.5.53", features = ["derive"] } dotenvy = "0.15.7" poise = "0.6.1" serde = "1.0.228" -squad-quest = { version = "0.8.0", path = ".." } +squad-quest = { version = "0.9.0", path = ".." } tokio = { version = "1.48.0", features = ["rt-multi-thread"] } toml = "0.9.8" diff --git a/discord/src/commands/account.rs b/discord/src/commands/account.rs index 9033d6d..5027f79 100644 --- a/discord/src/commands/account.rs +++ b/discord/src/commands/account.rs @@ -40,6 +40,34 @@ fn account_user_id(account: &Account) -> UserId { UserId::new(account.id.clone().parse::().expect("automatically inserted")) } +#[poise::command( + prefix_command, + slash_command, + guild_only, + required_permissions = "ADMINISTRATOR", +)] +pub async fn reset( + ctx: Context<'_>, + who: UserId, +) -> Result<(), Error> { + let accounts = ctx.data().config.load_accounts(); + + let acc_id = format!("{}", who.get()); + + if let None = accounts.iter().find(|a| a.id == acc_id) { + return Err(Error::AccountNotFound); + } + + let mut path = ctx.data().config.full_accounts_path(); + path.push(format!("{acc_id}.toml")); + Account::delete(path)?; + + let reply_string = "User was successfully reset.".to_string(); + ctx.reply(reply_string).await?; + + Ok(()) +} + #[poise::command( prefix_command, slash_command, diff --git a/discord/src/commands/map.rs b/discord/src/commands/map.rs new file mode 100644 index 0000000..efa58f7 --- /dev/null +++ b/discord/src/commands/map.rs @@ -0,0 +1,67 @@ +use squad_quest::{SquadObject, map::Map}; + +use crate::{Context, account::fetch_or_init_account, error::Error}; + +#[poise::command( + prefix_command, + slash_command, + guild_only, +)] +pub async fn unlock( + ctx: Context<'_>, + id: u16, +) -> Result<(), Error> { + let conf = &ctx.data().config; + + let map_path = conf.full_map_path(); + let map = Map::load(map_path)?; + + let Some(room) = map.room.iter().find(|r| r.id == id) else { + return Err(Error::RoomNotFound(id)); + }; + + let acc_id = format!("{}", ctx.author().id.get()); + let mut account = fetch_or_init_account(conf, acc_id); + + if account.balance < room.value { + return Err(Error::InsufficientFunds(room.value)); + } + + map.unlock_room_for_account(id, &mut account)?; + + let account_path = conf.full_accounts_path(); + account.save(account_path)?; + + let reply_string = format!("Unlocked room #{id}. Your balance: {} points", account.balance); + ctx.reply(reply_string).await?; + + Ok(()) +} + +#[poise::command( + prefix_command, + slash_command, + guild_only, +)] +pub async fn r#move( + ctx: Context<'_>, + id: u16, +) -> Result<(), Error> { + let conf = &ctx.data().config; + + let acc_id = format!("{}", ctx.author().id.get()); + let mut account = fetch_or_init_account(conf, acc_id); + + if let None = account.rooms_unlocked.iter().find(|rid| **rid == id) { + return Err(Error::RoomNotFound(id)); + } + + account.location = id; + let account_path = conf.full_accounts_path(); + account.save(account_path)?; + + let reply_string = format!("Moved to room #{id}."); + ctx.reply(reply_string).await?; + + Ok(()) +} diff --git a/discord/src/commands/mod.rs b/discord/src/commands/mod.rs index 5f57941..608ee48 100644 --- a/discord/src/commands/mod.rs +++ b/discord/src/commands/mod.rs @@ -8,6 +8,7 @@ pub mod init; pub mod answer; pub mod social; pub mod account; +pub mod map; #[poise::command(prefix_command)] pub async fn register(ctx: Context<'_>) -> Result<(), Error> { @@ -15,6 +16,21 @@ pub async fn register(ctx: Context<'_>) -> Result<(), Error> { Ok(()) } +#[poise::command( + prefix_command, + slash_command, +)] +pub async fn info(ctx: Context<'_>) -> Result<(), Error> { + let reply_string = format!("\ + SquadQuest version {ver}\n\ + Find the map here: {url}", + ver = env!("CARGO_PKG_VERSION"), + url = "not implemented yet!", + ); + ctx.say(reply_string).await?; + Ok(()) +} + pub async fn error_handler(error: poise::FrameworkError<'_, Data, Error>) { eprintln!("ERROR:"); print_error_recursively(&error); diff --git a/discord/src/error.rs b/discord/src/error.rs index 03881e7..1cb3927 100644 --- a/discord/src/error.rs +++ b/discord/src/error.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use poise::serenity_prelude as serenity; +use squad_quest::error::MapError; #[non_exhaustive] #[derive(Debug)] @@ -15,6 +16,9 @@ pub enum Error { SquadQuestError(squad_quest::error::Error), AccountNotFound, InsufficientFunds(u32), + RoomNotFound(u16), + RoomAlreadyUnlocked(u16), + CannotReach(u16), } impl From for Error { @@ -29,6 +33,18 @@ impl From for Error { } } +impl From for Error { + fn from(value: squad_quest::error::MapError) -> Self { + match value { + MapError::RoomNotFound(id) => Self::RoomNotFound(id), + MapError::RoomAlreadyUnlocked(id, _) => Self::RoomAlreadyUnlocked(id), + MapError::InsufficientFunds(_, _, amount) => Self::InsufficientFunds(amount), + MapError::CannotReach(id, _) => Self::CannotReach(id), + _ => unreachable!(), + } + } +} + impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -41,7 +57,10 @@ impl Display for Error { Self::SerenityError(_) => write!(f, "discord interaction error"), Self::SquadQuestError(_) => write!(f, "internal logic error"), Self::AccountNotFound => write!(f, "account not found"), - Self::InsufficientFunds(amount) => write!(f, "account does not have {amount} points"), + Self::InsufficientFunds(amount) => write!(f, "user does not have {amount} points"), + Self::RoomNotFound(id) => write!(f, "room #{id} not found"), + Self::RoomAlreadyUnlocked(id) => write!(f, "room #{id} is already unlocked for this user"), + Self::CannotReach(id) => write!(f, "user cannot reach room #{id}"), } } } @@ -56,7 +75,10 @@ impl std::error::Error for Error { Self::NoChannelOrUser | Self::BothChannelAndUser | Self::AccountNotFound | - Self::InsufficientFunds(_) => None, + Self::InsufficientFunds(_) | + Self::RoomNotFound(_) | + Self::RoomAlreadyUnlocked(_) | + Self::CannotReach(_) => None, Self::SerenityError(error) => Some(error), Self::SquadQuestError(error) => Some(error), } diff --git a/discord/src/main.rs b/discord/src/main.rs index cb73124..d37aa0d 100644 --- a/discord/src/main.rs +++ b/discord/src/main.rs @@ -42,11 +42,15 @@ async fn main() { commands: vec![ commands::quest::quest(), commands::register(), + commands::info(), commands::init::init(), commands::answer::answer(), commands::social::social(), commands::account::scoreboard(), commands::account::balance(), + commands::account::reset(), + commands::map::unlock(), + commands::map::r#move(), ], ..Default::default() }) diff --git a/src/error.rs b/src/error.rs index 1cc4654..f4f1bc3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -69,8 +69,10 @@ pub enum MapError { RoomNotFound(u16), /// Room (self.0) is already unlocked on account (self.1) RoomAlreadyUnlocked(u16, String), - /// Account (self.1) does not have much money (self.0) - InsufficientFunds(u16, String), + /// Account (self.1) does not have much money (self.0), expected (self.2) + InsufficientFunds(u16, String, u32), + /// Account (self.1) can't reach room (self.0) + CannotReach(u16, String), } impl fmt::Display for MapError { @@ -78,7 +80,8 @@ impl fmt::Display for MapError { match self { Self::RoomNotFound(id) => write!(f, "could not find room #{id}"), Self::RoomAlreadyUnlocked(room_id, account_id) => write!(f, "room #{room_id} is already unlocked on account \"{account_id}\""), - Self::InsufficientFunds(room_id, account_id) => write!(f, "account \"{account_id}\" does not have enough money to unlock room #{room_id}"), + Self::InsufficientFunds(room_id, account_id, amount) => write!(f, "account \"{account_id}\" does not have enough money to unlock room #{room_id}, expected {amount}"), + Self::CannotReach(room, account) => write!(f, "account \"{account}\" cannot reach room #{room}"), } } } diff --git a/src/map/mod.rs b/src/map/mod.rs index 69ccb68..1ed9aaf 100644 --- a/src/map/mod.rs +++ b/src/map/mod.rs @@ -99,8 +99,15 @@ impl Map { return Err(MapError::RoomAlreadyUnlocked(room_id, account.id.clone())); } + // Room 0 will always be reachable + let reachable = room.reachable_for(&account) || room.id == 0; + + if !reachable { + return Err(MapError::CannotReach(room_id, account.id.clone())); + } + if account.balance < room.value { - return Err(MapError::InsufficientFunds(room_id, account.id.clone())); + return Err(MapError::InsufficientFunds(room_id, account.id.clone(), room.value)); } account.balance -= room.value; @@ -141,3 +148,14 @@ impl Default for Room { } } } + +impl Room { + fn reachable_for(&self, account: &Account) -> bool { + for rid in account.rooms_unlocked.iter() { + if self.children.contains(&rid) { + return true; + } + } + false + } +}