feat: Completed commands list

- Added MapError::CannotReach variant
- Updated Map::unlock_room_for_account to check reachableness
- Added /info command
- Added /unlock command
- Added /move command
- Added /reset command
This commit is contained in:
Alexey 2025-12-15 15:19:07 +03:00
commit b6ea2d8958
11 changed files with 170 additions and 12 deletions

6
Cargo.lock generated
View file

@ -1599,7 +1599,7 @@ dependencies = [
[[package]] [[package]]
name = "squad-quest" name = "squad-quest"
version = "0.8.0" version = "0.9.0"
dependencies = [ dependencies = [
"serde", "serde",
"toml", "toml",
@ -1607,7 +1607,7 @@ dependencies = [
[[package]] [[package]]
name = "squad-quest-cli" name = "squad-quest-cli"
version = "0.8.0" version = "0.9.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@ -1618,7 +1618,7 @@ dependencies = [
[[package]] [[package]]
name = "squad-quest-discord" name = "squad-quest-discord"
version = "0.8.0" version = "0.9.0"
dependencies = [ dependencies = [
"clap", "clap",
"dotenvy", "dotenvy",

View file

@ -2,7 +2,7 @@
members = ["cli", "discord"] members = ["cli", "discord"]
[workspace.package] [workspace.package]
version = "0.8.0" version = "0.9.0"
edition = "2024" edition = "2024"
repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest" repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest"
license = "MIT" license = "MIT"

View file

@ -9,5 +9,5 @@ license.workspace = true
chrono = "0.4.42" chrono = "0.4.42"
clap = { version = "4.5.53", features = ["derive"] } clap = { version = "4.5.53", features = ["derive"] }
serde = { version = "1.0.228", 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" toml = "0.9.8"

View file

@ -10,6 +10,6 @@ clap = { version = "4.5.53", features = ["derive"] }
dotenvy = "0.15.7" dotenvy = "0.15.7"
poise = "0.6.1" poise = "0.6.1"
serde = "1.0.228" 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"] } tokio = { version = "1.48.0", features = ["rt-multi-thread"] }
toml = "0.9.8" toml = "0.9.8"

View file

@ -40,6 +40,34 @@ fn account_user_id(account: &Account) -> UserId {
UserId::new(account.id.clone().parse::<u64>().expect("automatically inserted")) UserId::new(account.id.clone().parse::<u64>().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( #[poise::command(
prefix_command, prefix_command,
slash_command, slash_command,

View file

@ -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(())
}

View file

@ -8,6 +8,7 @@ pub mod init;
pub mod answer; pub mod answer;
pub mod social; pub mod social;
pub mod account; pub mod account;
pub mod map;
#[poise::command(prefix_command)] #[poise::command(prefix_command)]
pub async fn register(ctx: Context<'_>) -> Result<(), Error> { pub async fn register(ctx: Context<'_>) -> Result<(), Error> {
@ -15,6 +16,21 @@ pub async fn register(ctx: Context<'_>) -> Result<(), Error> {
Ok(()) 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>) { pub async fn error_handler(error: poise::FrameworkError<'_, Data, Error>) {
eprintln!("ERROR:"); eprintln!("ERROR:");
print_error_recursively(&error); print_error_recursively(&error);

View file

@ -1,6 +1,7 @@
use std::fmt::Display; use std::fmt::Display;
use poise::serenity_prelude as serenity; use poise::serenity_prelude as serenity;
use squad_quest::error::MapError;
#[non_exhaustive] #[non_exhaustive]
#[derive(Debug)] #[derive(Debug)]
@ -15,6 +16,9 @@ pub enum Error {
SquadQuestError(squad_quest::error::Error), SquadQuestError(squad_quest::error::Error),
AccountNotFound, AccountNotFound,
InsufficientFunds(u32), InsufficientFunds(u32),
RoomNotFound(u16),
RoomAlreadyUnlocked(u16),
CannotReach(u16),
} }
impl From<serenity::Error> for Error { impl From<serenity::Error> for Error {
@ -29,6 +33,18 @@ impl From<squad_quest::error::Error> for Error {
} }
} }
impl From<squad_quest::error::MapError> 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 { impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -41,7 +57,10 @@ impl Display for Error {
Self::SerenityError(_) => write!(f, "discord interaction error"), Self::SerenityError(_) => write!(f, "discord interaction error"),
Self::SquadQuestError(_) => write!(f, "internal logic error"), Self::SquadQuestError(_) => write!(f, "internal logic error"),
Self::AccountNotFound => write!(f, "account not found"), 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::NoChannelOrUser |
Self::BothChannelAndUser | Self::BothChannelAndUser |
Self::AccountNotFound | Self::AccountNotFound |
Self::InsufficientFunds(_) => None, Self::InsufficientFunds(_) |
Self::RoomNotFound(_) |
Self::RoomAlreadyUnlocked(_) |
Self::CannotReach(_) => None,
Self::SerenityError(error) => Some(error), Self::SerenityError(error) => Some(error),
Self::SquadQuestError(error) => Some(error), Self::SquadQuestError(error) => Some(error),
} }

View file

@ -42,11 +42,15 @@ async fn main() {
commands: vec![ commands: vec![
commands::quest::quest(), commands::quest::quest(),
commands::register(), commands::register(),
commands::info(),
commands::init::init(), commands::init::init(),
commands::answer::answer(), commands::answer::answer(),
commands::social::social(), commands::social::social(),
commands::account::scoreboard(), commands::account::scoreboard(),
commands::account::balance(), commands::account::balance(),
commands::account::reset(),
commands::map::unlock(),
commands::map::r#move(),
], ],
..Default::default() ..Default::default()
}) })

View file

@ -69,8 +69,10 @@ pub enum MapError {
RoomNotFound(u16), RoomNotFound(u16),
/// Room (self.0) is already unlocked on account (self.1) /// Room (self.0) is already unlocked on account (self.1)
RoomAlreadyUnlocked(u16, String), RoomAlreadyUnlocked(u16, String),
/// Account (self.1) does not have much money (self.0) /// Account (self.1) does not have much money (self.0), expected (self.2)
InsufficientFunds(u16, String), InsufficientFunds(u16, String, u32),
/// Account (self.1) can't reach room (self.0)
CannotReach(u16, String),
} }
impl fmt::Display for MapError { impl fmt::Display for MapError {
@ -78,7 +80,8 @@ impl fmt::Display for MapError {
match self { match self {
Self::RoomNotFound(id) => write!(f, "could not find room #{id}"), 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::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}"),
} }
} }
} }

View file

@ -99,8 +99,15 @@ impl Map {
return Err(MapError::RoomAlreadyUnlocked(room_id, account.id.clone())); 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 { 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; 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
}
}