Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1db7ce877e | |||
| 2640821a05 | |||
| d188bba16e | |||
| d584340f01 | |||
| 81a9ec0c50 |
20 changed files with 480 additions and 88 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -2140,7 +2140,7 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "squad-quest"
|
name = "squad-quest"
|
||||||
version = "0.11.0"
|
version = "0.12.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"toml 0.9.10+spec-1.1.0",
|
"toml 0.9.10+spec-1.1.0",
|
||||||
|
|
@ -2148,7 +2148,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "squad-quest-cli"
|
name = "squad-quest-cli"
|
||||||
version = "0.11.0"
|
version = "0.12.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
|
@ -2159,7 +2159,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "squad-quest-discord"
|
name = "squad-quest-discord"
|
||||||
version = "0.11.0"
|
version = "0.12.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
members = ["cli", "discord"]
|
members = ["cli", "discord"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.11.0"
|
version = "0.12.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest"
|
repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest"
|
||||||
homepage = "https://2ndbeam.ru/git/2ndbeam/squad-quest"
|
homepage = "https://2ndbeam.ru/git/2ndbeam/squad-quest"
|
||||||
|
|
|
||||||
|
|
@ -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.11.0", path = ".." }
|
squad-quest = { version = "0.12.0", path = ".." }
|
||||||
toml = "0.9.8"
|
toml = "0.9.8"
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,9 @@ pub struct QuestCreateArgs {
|
||||||
/// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24)
|
/// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24)
|
||||||
#[arg(short,long,value_parser = parse_date)]
|
#[arg(short,long,value_parser = parse_date)]
|
||||||
pub deadline: Option<Date>,
|
pub deadline: Option<Date>,
|
||||||
|
/// Limit on how many users can solve the quest (0 = no limit)
|
||||||
|
#[arg(short,long)]
|
||||||
|
pub limit: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
@ -113,6 +116,9 @@ pub struct QuestUpdateArgs {
|
||||||
/// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24)
|
/// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24)
|
||||||
#[arg(long,value_parser = parse_date)]
|
#[arg(long,value_parser = parse_date)]
|
||||||
pub deadline: Option<Date>,
|
pub deadline: Option<Date>,
|
||||||
|
/// Limit on how many users can solve the quest (0 = no limit)
|
||||||
|
#[arg(long)]
|
||||||
|
pub limit: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,7 @@ fn main() {
|
||||||
public: args.public,
|
public: args.public,
|
||||||
available_on: args.available.clone(),
|
available_on: args.available.clone(),
|
||||||
deadline: args.deadline.clone(),
|
deadline: args.deadline.clone(),
|
||||||
|
limit: args.limit.unwrap_or_default(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -171,6 +172,7 @@ fn main() {
|
||||||
public: args.public.unwrap_or(quest.public),
|
public: args.public.unwrap_or(quest.public),
|
||||||
available_on: args.available.clone().or(quest.available_on.clone()),
|
available_on: args.available.clone().or(quest.available_on.clone()),
|
||||||
deadline: args.deadline.clone().or(quest.deadline.clone()),
|
deadline: args.deadline.clone().or(quest.deadline.clone()),
|
||||||
|
limit: args.limit.unwrap_or_default(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -284,10 +286,6 @@ fn main() {
|
||||||
do_and_log(account.save(path), !cli.quiet, format!("Updated balance of account \"{}\".", account.id));
|
do_and_log(account.save(path), !cli.quiet, format!("Updated balance of account \"{}\".", account.id));
|
||||||
},
|
},
|
||||||
AccountCommands::Complete(args) => {
|
AccountCommands::Complete(args) => {
|
||||||
let Some(account) = accounts.iter_mut().find(|a| a.id == args.account) else {
|
|
||||||
if !cli.quiet { eprintln!("Error: account \"{}\" not found.", args.account); }
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let quests = config.load_quests();
|
let quests = config.load_quests();
|
||||||
|
|
||||||
|
|
@ -299,9 +297,17 @@ fn main() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
match quest.complete_for_account(account) {
|
let result = quest.complete_for_account(&args.account, &mut accounts);
|
||||||
|
|
||||||
|
match result {
|
||||||
Err(error) if !cli.quiet => println!("Error: {error}"),
|
Err(error) if !cli.quiet => println!("Error: {error}"),
|
||||||
Ok(_) => do_and_log(account.save(path), !cli.quiet, format!("Completed quest #{} on account \"{}\".", args.quest, account.id)),
|
Ok(_) => {
|
||||||
|
let Some(account) = accounts.iter_mut().find(|a| a.id == args.account) else {
|
||||||
|
if !cli.quiet { eprintln!("Error: account \"{}\" not found.", args.account); }
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
do_and_log(account.save(path), !cli.quiet, format!("Completed quest #{} on account \"{}\".", args.quest, account.id));
|
||||||
|
},
|
||||||
_ => {},
|
_ => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,6 @@ poise = "0.6.1"
|
||||||
rocket = { version = "0.5.1", features = ["json"] }
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
serde = "1.0.228"
|
serde = "1.0.228"
|
||||||
serde_json = "1.0.146"
|
serde_json = "1.0.146"
|
||||||
squad-quest = { version = "0.11.0", path = ".." }
|
squad-quest = { version = "0.12.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"
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,14 @@ use std::collections::HashMap;
|
||||||
use poise::serenity_prelude::{User, UserId};
|
use poise::serenity_prelude::{User, UserId};
|
||||||
use squad_quest::{account::Account, config::Config, map::Map};
|
use squad_quest::{account::Account, config::Config, map::Map};
|
||||||
|
|
||||||
pub fn fetch_or_init_account(conf: &Config, id: String, user: Option<&User>) -> Account {
|
/// Returns Ok(account) if account was found or Err(new_account) if not
|
||||||
|
pub fn fetch_or_init_account(conf: &Config, id: &str, user: Option<&User>) -> Result<Account, Account> {
|
||||||
let accounts = conf.load_accounts();
|
let accounts = conf.load_accounts();
|
||||||
|
|
||||||
|
if let Some(account) = accounts.iter().find(|a| a.id == id) {
|
||||||
|
return Ok(account.clone());
|
||||||
|
}
|
||||||
|
|
||||||
let mut data: HashMap<String, String> = HashMap::new();
|
let mut data: HashMap<String, String> = HashMap::new();
|
||||||
|
|
||||||
if let Some(user) = user {
|
if let Some(user) = user {
|
||||||
|
|
@ -14,14 +20,13 @@ pub fn fetch_or_init_account(conf: &Config, id: String, user: Option<&User>) ->
|
||||||
data.insert("name".to_string(), name);
|
data.insert("name".to_string(), name);
|
||||||
}
|
}
|
||||||
|
|
||||||
match accounts.iter().find(|a| a.id == id) {
|
let new_account = Account {
|
||||||
Some(a) => a.clone(),
|
id: id.to_string(),
|
||||||
None => Account {
|
data: Some(data),
|
||||||
id,
|
..Default::default()
|
||||||
data: Some(data),
|
};
|
||||||
..Default::default()
|
|
||||||
},
|
Err(new_account)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn account_rooms_value(account: &Account, map: &Map) -> u32 {
|
pub fn account_rooms_value(account: &Account, map: &Map) -> u32 {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use poise::serenity_prelude::User;
|
use poise::serenity_prelude::User;
|
||||||
use squad_quest::{SquadObject, account::Account, map::Map};
|
use squad_quest::{SquadObject, account::Account, map::Map};
|
||||||
|
|
||||||
use crate::{Context, Error, account::{account_full_balance, account_user_id, fetch_or_init_account}, strings::StringFormatter};
|
use crate::{Context, Error, account::{account_full_balance, account_user_id}, strings::StringFormatter, commands::guild};
|
||||||
|
|
||||||
async fn account_balance_string(
|
async fn account_balance_string(
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
|
|
@ -26,6 +26,7 @@ async fn account_balance_string(
|
||||||
prefix_command,
|
prefix_command,
|
||||||
slash_command,
|
slash_command,
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
required_permissions = "ADMINISTRATOR",
|
required_permissions = "ADMINISTRATOR",
|
||||||
name_localized("ru", "сбросить"),
|
name_localized("ru", "сбросить"),
|
||||||
description_localized("ru", "Сбросить аккаунт пользователя, вкл. баланс, открытые комнаты и пройденные квесты"),
|
description_localized("ru", "Сбросить аккаунт пользователя, вкл. баланс, открытые комнаты и пройденные квесты"),
|
||||||
|
|
@ -63,6 +64,7 @@ pub async fn reset(
|
||||||
prefix_command,
|
prefix_command,
|
||||||
slash_command,
|
slash_command,
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
name_localized("ru", "счет"),
|
name_localized("ru", "счет"),
|
||||||
description_localized("ru", "Отобразить таблицу лидеров"),
|
description_localized("ru", "Отобразить таблицу лидеров"),
|
||||||
)]
|
)]
|
||||||
|
|
@ -106,6 +108,7 @@ pub async fn scoreboard(
|
||||||
prefix_command,
|
prefix_command,
|
||||||
slash_command,
|
slash_command,
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
subcommands("give", "set"),
|
subcommands("give", "set"),
|
||||||
name_localized("ru", "баланс"),
|
name_localized("ru", "баланс"),
|
||||||
)]
|
)]
|
||||||
|
|
@ -120,6 +123,7 @@ pub async fn balance(
|
||||||
prefix_command,
|
prefix_command,
|
||||||
slash_command,
|
slash_command,
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
name_localized("ru", "передать"),
|
name_localized("ru", "передать"),
|
||||||
description_localized("ru", "Передать очки другому пользователю"),
|
description_localized("ru", "Передать очки другому пользователю"),
|
||||||
)]
|
)]
|
||||||
|
|
@ -142,30 +146,40 @@ pub async fn give(
|
||||||
let mut accounts = config.load_accounts();
|
let mut accounts = config.load_accounts();
|
||||||
|
|
||||||
let user_id = format!("{}", ctx.author().id.get());
|
let user_id = format!("{}", ctx.author().id.get());
|
||||||
|
let strings = &ctx.data().strings;
|
||||||
let mut user_account = fetch_or_init_account(config, user_id, Some(ctx.author()));
|
let formatter: StringFormatter;
|
||||||
|
let accounts_path = config.full_accounts_path();
|
||||||
|
|
||||||
let who_id = format!("{}", who.id.get());
|
let who_id = format!("{}", who.id.get());
|
||||||
let Some(other_account) = accounts.iter_mut().find(|a| a.id == who_id ) else {
|
|
||||||
return Err(Error::AccountNotFound);
|
|
||||||
};
|
|
||||||
|
|
||||||
if user_account.balance < amount {
|
if let None = accounts.iter().find(|a| a.id == who_id ) {
|
||||||
return Err(Error::InsufficientFunds(amount));
|
return Err(Error::AccountNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
user_account.balance -= amount;
|
{
|
||||||
|
let Some(user_account) = accounts.iter_mut().find(|a| a.id == user_id) else {
|
||||||
|
return Err(Error::AccountNotFound);
|
||||||
|
};
|
||||||
|
|
||||||
|
if user_account.balance < amount {
|
||||||
|
return Err(Error::InsufficientFunds(amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
user_account.balance -= amount;
|
||||||
|
user_account.save(accounts_path.clone())?;
|
||||||
|
|
||||||
|
formatter = strings.formatter()
|
||||||
|
.value(amount)
|
||||||
|
.user(&who)
|
||||||
|
.current_balance(&user_account);
|
||||||
|
}
|
||||||
|
|
||||||
|
let other_account = accounts.iter_mut().find(|a| a.id == who_id ).expect("We already checked its existence earlier");
|
||||||
|
|
||||||
other_account.balance += amount;
|
other_account.balance += amount;
|
||||||
|
|
||||||
let accounts_path = config.full_accounts_path();
|
|
||||||
user_account.save(accounts_path.clone())?;
|
|
||||||
other_account.save(accounts_path)?;
|
other_account.save(accounts_path)?;
|
||||||
|
|
||||||
let strings = &ctx.data().strings;
|
|
||||||
let formatter = strings.formatter()
|
|
||||||
.user(&who)
|
|
||||||
.value(amount)
|
|
||||||
.current_balance(&user_account);
|
|
||||||
|
|
||||||
let reply_string = formatter.fmt(&strings.account.give_pt);
|
let reply_string = formatter.fmt(&strings.account.give_pt);
|
||||||
ctx.reply(reply_string).await?;
|
ctx.reply(reply_string).await?;
|
||||||
|
|
@ -178,6 +192,7 @@ pub async fn give(
|
||||||
prefix_command,
|
prefix_command,
|
||||||
slash_command,
|
slash_command,
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
required_permissions = "ADMINISTRATOR",
|
required_permissions = "ADMINISTRATOR",
|
||||||
name_localized("ru", "установить"),
|
name_localized("ru", "установить"),
|
||||||
description_localized("ru", "Устанавливает текущий баланс пользователя"),
|
description_localized("ru", "Устанавливает текущий баланс пользователя"),
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
use poise::serenity_prelude::{Attachment, ComponentInteractionCollector, CreateActionRow, CreateAttachment, CreateButton, CreateMessage, EditMessage};
|
use poise::serenity_prelude::{Attachment, ComponentInteractionCollector, CreateActionRow, CreateAttachment, CreateButton, CreateMessage, EditMessage};
|
||||||
use squad_quest::SquadObject;
|
use squad_quest::SquadObject;
|
||||||
|
|
||||||
use crate::{Context, Error, account::fetch_or_init_account};
|
use crate::{account::fetch_or_init_account, commands::{guild, quest::update_quest_message}, Context, Error};
|
||||||
|
|
||||||
/// Send an answer to the quest for review
|
/// Send an answer to the quest for review
|
||||||
#[poise::command(
|
#[poise::command(
|
||||||
prefix_command,
|
prefix_command,
|
||||||
slash_command,
|
slash_command,
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
name_localized("ru", "ответить"),
|
name_localized("ru", "ответить"),
|
||||||
description_localized("ru", "Отправить ответ на квест на проверку"),
|
description_localized("ru", "Отправить ответ на квест на проверку"),
|
||||||
)]
|
)]
|
||||||
|
|
@ -34,18 +35,33 @@ pub async fn answer(
|
||||||
#[description_localized("ru", "Вложение к ответу на квест")]
|
#[description_localized("ru", "Вложение к ответу на квест")]
|
||||||
file3: Option<Attachment>,
|
file3: Option<Attachment>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut account = fetch_or_init_account(&ctx.data().config, ctx.author().id.to_string(), Some(ctx.author()));
|
|
||||||
|
|
||||||
if let Some(_) = account.quests_completed.iter().find(|qid| **qid == quest_id) {
|
|
||||||
return Err(Error::QuestIsCompleted(quest_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
let quests = ctx.data().config.load_quests();
|
let quests = ctx.data().config.load_quests();
|
||||||
let Some(quest) = quests.iter()
|
let Some(quest) = quests.iter()
|
||||||
.filter(|q| q.public)
|
.filter(|q| q.public)
|
||||||
.find(|q| q.id == quest_id) else {
|
.find(|q| q.id == quest_id) else {
|
||||||
return Err(Error::QuestNotFound(quest_id));
|
return Err(Error::QuestNotFound(quest_id));
|
||||||
};
|
};
|
||||||
|
{
|
||||||
|
let accounts = ctx.data().config.load_accounts();
|
||||||
|
let completed_times = accounts.iter().filter(|a| a.has_completed_quest(&quest)).count() as u8;
|
||||||
|
if quest.limit > 0 && completed_times >= quest.limit {
|
||||||
|
return Err(Error::QuestLimitExceeded(quest.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_id = ctx.author().id.to_string();
|
||||||
|
match fetch_or_init_account(&ctx.data().config, &user_id, Some(ctx.author())) {
|
||||||
|
Ok(account) => {
|
||||||
|
if let Some(_) = account.quests_completed.iter().find(|qid| **qid == quest_id) {
|
||||||
|
return Err(Error::QuestIsCompleted(quest_id));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(new_account) => {
|
||||||
|
let path = ctx.data().config.full_accounts_path();
|
||||||
|
new_account.save(path)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
let mut files: Vec<Attachment> = Vec::new();
|
let mut files: Vec<Attachment> = Vec::new();
|
||||||
for file in [file1, file2, file3] {
|
for file in [file1, file2, file3] {
|
||||||
|
|
@ -134,16 +150,20 @@ pub async fn answer(
|
||||||
let content: String;
|
let content: String;
|
||||||
if is_approved {
|
if is_approved {
|
||||||
let mut no_errors = true;
|
let mut no_errors = true;
|
||||||
if let Err(error) = quest.complete_for_account(&mut account) {
|
let mut accounts = ctx.data().config.load_accounts();
|
||||||
|
if let Err(error) = quest.complete_for_account(&ctx.author().id.to_string(), &mut accounts) {
|
||||||
eprintln!("{error}");
|
eprintln!("{error}");
|
||||||
no_errors = false;
|
no_errors = false;
|
||||||
};
|
};
|
||||||
|
let account = accounts.iter_mut().find(|a| a.id == user_id).expect("we done fetch_or_init earlier");
|
||||||
let path = ctx.data().config.full_accounts_path();
|
let path = ctx.data().config.full_accounts_path();
|
||||||
if let Err(error) = account.save(path) {
|
if let Err(error) = account.save(path) {
|
||||||
eprintln!("{error}");
|
eprintln!("{error}");
|
||||||
no_errors = false;
|
no_errors = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
update_quest_message(ctx, &quest).await?;
|
||||||
|
|
||||||
formatter = formatter.current_balance(&account);
|
formatter = formatter.current_balance(&account);
|
||||||
|
|
||||||
if no_errors {
|
if no_errors {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use poise::{CreateReply, serenity_prelude::ChannelId};
|
||||||
use squad_quest::SquadObject;
|
use squad_quest::SquadObject;
|
||||||
use toml::value::Time;
|
use toml::value::Time;
|
||||||
|
|
||||||
use crate::{Context, Error, timer::DailyTimer};
|
use crate::{Context, Error, timer::DailyTimer, commands::guild};
|
||||||
|
|
||||||
/// Set channels to post quests and answers to
|
/// Set channels to post quests and answers to
|
||||||
#[poise::command(
|
#[poise::command(
|
||||||
|
|
@ -12,6 +12,7 @@ use crate::{Context, Error, timer::DailyTimer};
|
||||||
slash_command,
|
slash_command,
|
||||||
required_permissions = "ADMINISTRATOR",
|
required_permissions = "ADMINISTRATOR",
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
name_localized("ru", "инит"),
|
name_localized("ru", "инит"),
|
||||||
description_localized("ru", "Установить каналы для публикации квестов и ответов"),
|
description_localized("ru", "Установить каналы для публикации квестов и ответов"),
|
||||||
)]
|
)]
|
||||||
|
|
@ -75,6 +76,7 @@ fn seconds(time: Time) -> u64 {
|
||||||
slash_command,
|
slash_command,
|
||||||
required_permissions = "ADMINISTRATOR",
|
required_permissions = "ADMINISTRATOR",
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
name_localized("ru", "таймер"),
|
name_localized("ru", "таймер"),
|
||||||
description_localized("ru", "Включить таймер публикации по заданной временной метке UTC (МСК -3)"),
|
description_localized("ru", "Включить таймер публикации по заданной временной метке UTC (МСК -3)"),
|
||||||
)]
|
)]
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
|
use poise::{serenity_prelude::{Attachment, CreateAttachment, CreateMessage}, CreateReply};
|
||||||
use squad_quest::{SquadObject, map::Map};
|
use squad_quest::{SquadObject, map::Map};
|
||||||
|
|
||||||
use crate::{Context, account::fetch_or_init_account, error::Error};
|
use crate::{Context, account::fetch_or_init_account, error::Error, commands::guild};
|
||||||
|
|
||||||
/// Unlock specified room if it is reachable and you have required amount of points
|
/// Unlock specified room if it is reachable and you have required amount of points
|
||||||
#[poise::command(
|
#[poise::command(
|
||||||
prefix_command,
|
prefix_command,
|
||||||
slash_command,
|
slash_command,
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
name_localized("ru", "открыть"),
|
name_localized("ru", "открыть"),
|
||||||
description_localized("ru", "Открывает указанную комнату, если хватает очков и до нее можно добраться"),
|
description_localized("ru", "Открывает указанную комнату, если хватает очков и до нее можно добраться"),
|
||||||
)]
|
)]
|
||||||
|
|
@ -27,7 +29,10 @@ pub async fn unlock(
|
||||||
};
|
};
|
||||||
|
|
||||||
let acc_id = format!("{}", ctx.author().id.get());
|
let acc_id = format!("{}", ctx.author().id.get());
|
||||||
let mut account = fetch_or_init_account(conf, acc_id, Some(ctx.author()));
|
let mut account = match fetch_or_init_account(conf, &acc_id, Some(ctx.author())) {
|
||||||
|
Ok(account) => account,
|
||||||
|
Err(account) => account,
|
||||||
|
};
|
||||||
|
|
||||||
if account.balance < room.value {
|
if account.balance < room.value {
|
||||||
return Err(Error::InsufficientFunds(room.value));
|
return Err(Error::InsufficientFunds(room.value));
|
||||||
|
|
@ -55,6 +60,7 @@ pub async fn unlock(
|
||||||
prefix_command,
|
prefix_command,
|
||||||
slash_command,
|
slash_command,
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
name_localized("ru", "пойти"),
|
name_localized("ru", "пойти"),
|
||||||
description_localized("ru", "Переместиться в другую разблокированную комнату"),
|
description_localized("ru", "Переместиться в другую разблокированную комнату"),
|
||||||
)]
|
)]
|
||||||
|
|
@ -68,7 +74,10 @@ pub async fn r#move(
|
||||||
let conf = &ctx.data().config;
|
let conf = &ctx.data().config;
|
||||||
|
|
||||||
let acc_id = format!("{}", ctx.author().id.get());
|
let acc_id = format!("{}", ctx.author().id.get());
|
||||||
let mut account = fetch_or_init_account(conf, acc_id, Some(ctx.author()));
|
let mut account = match fetch_or_init_account(conf, &acc_id, Some(ctx.author())) {
|
||||||
|
Ok(account) => account,
|
||||||
|
Err(account) => account,
|
||||||
|
};
|
||||||
|
|
||||||
if let None = account.rooms_unlocked.iter().find(|rid| **rid == id) {
|
if let None = account.rooms_unlocked.iter().find(|rid| **rid == id) {
|
||||||
return Err(Error::CannotReach(id));
|
return Err(Error::CannotReach(id));
|
||||||
|
|
@ -88,3 +97,99 @@ pub async fn r#move(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change avatar on web map
|
||||||
|
#[poise::command(
|
||||||
|
slash_command,
|
||||||
|
prefix_command,
|
||||||
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
|
name_localized("ru", "аватар"),
|
||||||
|
description_localized("ru", "Сменить аватар на веб карте"),
|
||||||
|
)]
|
||||||
|
pub async fn avatar(
|
||||||
|
ctx: Context<'_>,
|
||||||
|
#[description = "URL to the avatar"]
|
||||||
|
#[name_localized("ru", "ссылка")]
|
||||||
|
#[description_localized("ru", "Ссылка на аватар")]
|
||||||
|
url: Option<String>,
|
||||||
|
#[description = "Attachment to use as avatar"]
|
||||||
|
#[name_localized("ru", "вложение")]
|
||||||
|
#[description_localized("ru", "Вложение, используемое как аватар")]
|
||||||
|
attachment: Option<Attachment>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let user_id = ctx.author().id.to_string();
|
||||||
|
let mut accounts = ctx.data().config.load_accounts();
|
||||||
|
let Some(account) = accounts.iter_mut().find(|a| a.id == user_id) else {
|
||||||
|
return Err(Error::AccountNotFound);
|
||||||
|
};
|
||||||
|
|
||||||
|
if url.is_none() && attachment.is_none() {
|
||||||
|
return Err(Error::NoUrlOrAttachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if url.is_some() && attachment.is_some() {
|
||||||
|
return Err(Error::BothUrlAndAttachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
let strings = &ctx.data().strings;
|
||||||
|
let formatter = strings.formatter();
|
||||||
|
|
||||||
|
if let Some(url) = url {
|
||||||
|
let attachment = CreateAttachment::url(ctx, &url).await?;
|
||||||
|
let reply_string = formatter.fmt(&strings.map.processing_url);
|
||||||
|
let builder = CreateMessage::new()
|
||||||
|
.content(reply_string)
|
||||||
|
.add_file(attachment.clone());
|
||||||
|
let message = ctx.channel_id().send_message(ctx, builder).await?;
|
||||||
|
|
||||||
|
let attachment_check = message.attachments.first().expect("we just sent it");
|
||||||
|
if attachment_check.width.is_none()
|
||||||
|
|| !attachment_check.content_type
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|t| t.starts_with("image/")) {
|
||||||
|
message.delete(ctx).await?;
|
||||||
|
return Err(Error::NonImageAttachment);
|
||||||
|
}
|
||||||
|
let data = account.data.as_mut().expect("automatically created");
|
||||||
|
data.insert("avatar".to_string(), url);
|
||||||
|
|
||||||
|
message.delete(ctx).await?;
|
||||||
|
|
||||||
|
let reply_string = formatter.fmt(&strings.map.updated_avatar);
|
||||||
|
let builder = CreateReply::default()
|
||||||
|
.content(reply_string)
|
||||||
|
.attachment(attachment)
|
||||||
|
.reply(true);
|
||||||
|
ctx.send(builder).await?;
|
||||||
|
|
||||||
|
} else if let Some(attachment) = attachment {
|
||||||
|
if attachment.width.is_none()
|
||||||
|
|| !attachment.content_type
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|t| t.starts_with("image/")) {
|
||||||
|
return Err(Error::NonImageAttachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
let reply_string = formatter.fmt(&strings.map.updated_avatar);
|
||||||
|
let copied_attachment = CreateAttachment::url(ctx, &attachment.url).await?;
|
||||||
|
|
||||||
|
let data = account.data.as_mut().expect("automatically created");
|
||||||
|
data.insert("avatar".to_string(), attachment.url);
|
||||||
|
|
||||||
|
let path = ctx.data().config.full_accounts_path();
|
||||||
|
account.save(path)?;
|
||||||
|
|
||||||
|
let builder = CreateReply::default()
|
||||||
|
.content(reply_string)
|
||||||
|
.attachment(copied_attachment)
|
||||||
|
.reply(true);
|
||||||
|
|
||||||
|
ctx.send(builder).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = ctx.data().config.full_accounts_path();
|
||||||
|
account.save(path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use std::error::Error as StdError;
|
use std::error::Error as StdError;
|
||||||
use poise::CreateReply;
|
use poise::{CreateReply, serenity_prelude::GuildId};
|
||||||
|
use squad_quest::quest::Quest;
|
||||||
|
|
||||||
use crate::{Context, Data, Error};
|
use crate::{error, Context, Data, Error};
|
||||||
|
|
||||||
pub mod quest;
|
pub mod quest;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
|
|
@ -10,6 +11,18 @@ pub mod social;
|
||||||
pub mod account;
|
pub mod account;
|
||||||
pub mod map;
|
pub mod map;
|
||||||
|
|
||||||
|
pub async fn guild(ctx: Context<'_>) -> Result<bool, Error> {
|
||||||
|
let id = ctx.guild_id().expect("guild-only command");
|
||||||
|
let guard = ctx.data().discord.lock().expect("shouldn't be locked");
|
||||||
|
let expected_id = guard.guild;
|
||||||
|
|
||||||
|
if expected_id != GuildId::default() && id != expected_id {
|
||||||
|
return Err(Error::NotThisGuild);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
#[poise::command(prefix_command)]
|
#[poise::command(prefix_command)]
|
||||||
pub async fn register(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn register(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
poise::builtins::register_application_commands_buttons(ctx).await?;
|
poise::builtins::register_application_commands_buttons(ctx).await?;
|
||||||
|
|
@ -36,10 +49,45 @@ pub async fn error_handler(error: poise::FrameworkError<'_, Data, Error>) {
|
||||||
eprintln!("ERROR:");
|
eprintln!("ERROR:");
|
||||||
print_error_recursively(&error);
|
print_error_recursively(&error);
|
||||||
if let Some(ctx) = error.ctx() {
|
if let Some(ctx) = error.ctx() {
|
||||||
let response = match error {
|
let user = ctx.author().display_name();
|
||||||
poise::FrameworkError::Command { error, .. } => format!("Internal server error: {error}"),
|
|
||||||
_ => format!("Internal server error: {error}"),
|
eprintln!("User: {user} ({id})", id = ctx.author().id);
|
||||||
|
eprintln!("Invokation string: {}", ctx.invocation_string());
|
||||||
|
|
||||||
|
let strings = &ctx.data().strings;
|
||||||
|
|
||||||
|
let response = if let poise::FrameworkError::Command { error, .. } = error {
|
||||||
|
let formatter = match error {
|
||||||
|
error::Error::QuestNotFound(id) |
|
||||||
|
error::Error::QuestIsPublic(id) |
|
||||||
|
error::Error::QuestIsCompleted(id) |
|
||||||
|
error::Error::QuestLimitExceeded(id) =>
|
||||||
|
strings.formatter().quest(&Quest { id, ..Default::default() }),
|
||||||
|
|
||||||
|
error::Error::InsufficientFunds(amount) =>
|
||||||
|
strings.formatter().value(amount),
|
||||||
|
|
||||||
|
error::Error::RoomNotFound(value) |
|
||||||
|
error::Error::RoomAlreadyUnlocked(value) |
|
||||||
|
error::Error::CannotReach(value) =>
|
||||||
|
strings.formatter().value(value),
|
||||||
|
|
||||||
|
error::Error::SerenityError(ref error) =>
|
||||||
|
strings.formatter().text(error),
|
||||||
|
|
||||||
|
error::Error::SquadQuestError(ref error) =>
|
||||||
|
strings.formatter().text(error),
|
||||||
|
|
||||||
|
_ => strings.formatter(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let error_string = error.formattable_string(&strings.error);
|
||||||
|
formatter.fmt(error_string)
|
||||||
|
} else {
|
||||||
|
let formatter = strings.formatter().text(&error);
|
||||||
|
formatter.fmt(&strings.error.non_command_error)
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(error) = ctx.send(CreateReply::default().content(response).ephemeral(true)).await {
|
if let Err(error) = ctx.send(CreateReply::default().content(response).ephemeral(true)).await {
|
||||||
eprintln!("Couldn't send error message: {error}");
|
eprintln!("Couldn't send error message: {error}");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use std::{future, str::FromStr};
|
use std::{future, str::FromStr};
|
||||||
|
|
||||||
use poise::serenity_prelude::{CreateMessage, EditMessage, Message, futures::StreamExt};
|
use poise::serenity_prelude as serenity;
|
||||||
use squad_quest::{SquadObject, quest::{Quest, QuestDifficulty}};
|
use serenity::{CreateMessage, EditMessage, Message, futures::StreamExt};
|
||||||
|
use squad_quest::{account::Account, quest::{Quest, QuestDifficulty}, SquadObject};
|
||||||
use toml::value::Date;
|
use toml::value::Date;
|
||||||
use crate::{Context, Error};
|
use crate::{Context, Error,commands::guild};
|
||||||
|
|
||||||
async fn find_quest_message(ctx: Context<'_>, id: u16) -> Result<Option<Message>, Error>{
|
async fn find_quest_message(ctx: Context<'_>, id: u16) -> Result<Option<Message>, Error>{
|
||||||
ctx.defer().await?;
|
ctx.defer().await?;
|
||||||
|
|
@ -24,16 +25,42 @@ async fn find_quest_message(ctx: Context<'_>, id: u16) -> Result<Option<Message>
|
||||||
Ok(messages.first().cloned())
|
Ok(messages.first().cloned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_quest_message_content(ctx: Context<'_>, quest: &Quest) -> String {
|
fn make_quest_message_content(ctx: Context<'_>, quest: &Quest, accounts: &Option<Vec<Account>>) -> String {
|
||||||
let strings = &ctx.data().strings;
|
let strings = &ctx.data().strings;
|
||||||
let formatter = strings.formatter().quest(quest);
|
let formatter = match accounts {
|
||||||
|
Some(accounts) => strings.formatter().quest_full(quest, accounts),
|
||||||
|
None => strings.formatter().quest(quest),
|
||||||
|
};
|
||||||
formatter.fmt(&strings.quest.message_format)
|
formatter.fmt(&strings.quest.message_format)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn update_quest_message(ctx: Context<'_>, quest: &Quest) -> Result<(), Error> {
|
||||||
|
let strings = &ctx.data().strings;
|
||||||
|
let formatter = strings.formatter().quest(&quest);
|
||||||
|
let accounts = ctx.data().config.load_accounts();
|
||||||
|
let content = make_quest_message_content(ctx, &quest, &Some(accounts));
|
||||||
|
let builder = EditMessage::new().content(content);
|
||||||
|
|
||||||
|
let message = find_quest_message(ctx, quest.id).await?;
|
||||||
|
if let Some(mut message) = message {
|
||||||
|
return match message.edit(ctx, builder).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(error) => Err(error.into()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let reply_string = formatter.fmt(&strings.quest.message_not_found);
|
||||||
|
match ctx.reply(reply_string).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(error) => Err(error.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[poise::command(
|
#[poise::command(
|
||||||
prefix_command,
|
prefix_command,
|
||||||
slash_command,
|
slash_command,
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
subcommands("list", "create", "update", "publish", "delete"),
|
subcommands("list", "create", "update", "publish", "delete"),
|
||||||
required_permissions = "ADMINISTRATOR",
|
required_permissions = "ADMINISTRATOR",
|
||||||
name_localized("ru", "квест"),
|
name_localized("ru", "квест"),
|
||||||
|
|
@ -49,6 +76,7 @@ pub async fn quest(
|
||||||
prefix_command,
|
prefix_command,
|
||||||
slash_command,
|
slash_command,
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
required_permissions = "ADMINISTRATOR",
|
required_permissions = "ADMINISTRATOR",
|
||||||
name_localized("ru", "список"),
|
name_localized("ru", "список"),
|
||||||
description_localized("ru", "Вывести все квесты")
|
description_localized("ru", "Вывести все квесты")
|
||||||
|
|
@ -114,6 +142,7 @@ impl From<DateWrapper> for Date {
|
||||||
slash_command,
|
slash_command,
|
||||||
required_permissions = "ADMINISTRATOR",
|
required_permissions = "ADMINISTRATOR",
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
name_localized("ru", "создать"),
|
name_localized("ru", "создать"),
|
||||||
description_localized("ru", "Создать квест и получить его идентификатор"),
|
description_localized("ru", "Создать квест и получить его идентификатор"),
|
||||||
)]
|
)]
|
||||||
|
|
@ -143,6 +172,10 @@ pub async fn create(
|
||||||
#[name_localized("ru", "доступен")]
|
#[name_localized("ru", "доступен")]
|
||||||
#[description_localized("ru", "Дата публикации (в формате ГГГГ-ММ-ДД, напр. 2025-12-24)")]
|
#[description_localized("ru", "Дата публикации (в формате ГГГГ-ММ-ДД, напр. 2025-12-24)")]
|
||||||
available: Option<DateWrapper>,
|
available: Option<DateWrapper>,
|
||||||
|
#[description = "Limit how many users are allowed to complete the quest"]
|
||||||
|
#[name_localized("ru", "лимит")]
|
||||||
|
#[description_localized("ru", "Ограничение количества участников, которые могут выполнить квест")]
|
||||||
|
limit: Option<u8>,
|
||||||
/*
|
/*
|
||||||
#[description = "Deadline (in format of YYYY-MM-DD, e.g. 2025-12-24)"]
|
#[description = "Deadline (in format of YYYY-MM-DD, e.g. 2025-12-24)"]
|
||||||
#[name_localized("ru", "дедлайн")]
|
#[name_localized("ru", "дедлайн")]
|
||||||
|
|
@ -179,6 +212,7 @@ pub async fn create(
|
||||||
answer,
|
answer,
|
||||||
public: false,
|
public: false,
|
||||||
available_on,
|
available_on,
|
||||||
|
limit: limit.unwrap_or_default(),
|
||||||
//deadline,
|
//deadline,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
@ -202,6 +236,7 @@ pub async fn create(
|
||||||
slash_command,
|
slash_command,
|
||||||
required_permissions = "ADMINISTRATOR",
|
required_permissions = "ADMINISTRATOR",
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
name_localized("ru", "обновить"),
|
name_localized("ru", "обновить"),
|
||||||
description_localized("ru", "Обновить выбранные значения указанного квеста"),
|
description_localized("ru", "Обновить выбранные значения указанного квеста"),
|
||||||
)]
|
)]
|
||||||
|
|
@ -235,6 +270,10 @@ pub async fn update(
|
||||||
#[name_localized("ru", "доступен")]
|
#[name_localized("ru", "доступен")]
|
||||||
#[description_localized("ru", "Дата публикации (в формате ГГГГ-ММ-ДД, напр. 2025-12-24")]
|
#[description_localized("ru", "Дата публикации (в формате ГГГГ-ММ-ДД, напр. 2025-12-24")]
|
||||||
available: Option<DateWrapper>,
|
available: Option<DateWrapper>,
|
||||||
|
#[description = "Limit how many users are allowed to complete the quest"]
|
||||||
|
#[name_localized("ru", "лимит")]
|
||||||
|
#[description_localized("ru", "Ограничение количества участников, которые могут выполнить квест")]
|
||||||
|
limit: Option<u8>,
|
||||||
/*
|
/*
|
||||||
#[description = "Quest deadline (in format of YYYY-MM-DD, e.g. 2025-12-24)"]
|
#[description = "Quest deadline (in format of YYYY-MM-DD, e.g. 2025-12-24)"]
|
||||||
#[name_localized("ru", "дедлайн")]
|
#[name_localized("ru", "дедлайн")]
|
||||||
|
|
@ -260,15 +299,18 @@ pub async fn update(
|
||||||
};
|
};
|
||||||
|
|
||||||
let available_on: Option<Date>;
|
let available_on: Option<Date>;
|
||||||
|
let new_limit: u8;
|
||||||
//let dead_line: Option<Date>;
|
//let dead_line: Option<Date>;
|
||||||
|
|
||||||
match reset.unwrap_or(false) {
|
match reset.unwrap_or(false) {
|
||||||
true => {
|
true => {
|
||||||
available_on = None;
|
available_on = None;
|
||||||
|
new_limit = limit.unwrap_or_default();
|
||||||
//dead_line = None;
|
//dead_line = None;
|
||||||
},
|
},
|
||||||
false => {
|
false => {
|
||||||
available_on = available.map_or_else(|| quest.available_on.clone(), |v| Some(v.into()));
|
available_on = available.map_or_else(|| quest.available_on.clone(), |v| Some(v.into()));
|
||||||
|
new_limit = limit.unwrap_or(quest.limit);
|
||||||
//dead_line = deadline.map_or_else(|| quest.deadline.clone(), |v| Some(v.into()));
|
//dead_line = deadline.map_or_else(|| quest.deadline.clone(), |v| Some(v.into()));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -282,6 +324,7 @@ pub async fn update(
|
||||||
answer: answer.unwrap_or(quest.answer.clone()),
|
answer: answer.unwrap_or(quest.answer.clone()),
|
||||||
public: quest.public,
|
public: quest.public,
|
||||||
available_on,
|
available_on,
|
||||||
|
limit: new_limit,
|
||||||
//deadline: dead_line,
|
//deadline: dead_line,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
@ -290,16 +333,7 @@ pub async fn update(
|
||||||
let formatter = strings.formatter().quest(&new_quest);
|
let formatter = strings.formatter().quest(&new_quest);
|
||||||
|
|
||||||
if new_quest.public {
|
if new_quest.public {
|
||||||
let content = make_quest_message_content(ctx, &new_quest);
|
update_quest_message(ctx, &new_quest).await?;
|
||||||
let builder = EditMessage::new().content(content);
|
|
||||||
|
|
||||||
let message = find_quest_message(ctx, id).await?;
|
|
||||||
if let Some(mut message) = message {
|
|
||||||
message.edit(ctx, builder).await?;
|
|
||||||
} else {
|
|
||||||
let reply_string = formatter.fmt(&strings.quest.message_not_found);
|
|
||||||
ctx.reply(reply_string).await?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = conf.full_quests_path();
|
let path = conf.full_quests_path();
|
||||||
|
|
@ -310,13 +344,14 @@ pub async fn update(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn publish_inner(ctx: Context<'_>, quest: &mut Quest) -> Result<(), Error> {
|
pub async fn publish_inner(ctx: Context<'_>, quest: &mut Quest) -> Result<Message, Error> {
|
||||||
quest.public = true;
|
quest.public = true;
|
||||||
|
|
||||||
let quests_path = ctx.data().config.full_quests_path();
|
let quests_path = ctx.data().config.full_quests_path();
|
||||||
quest.save(quests_path)?;
|
quest.save(quests_path)?;
|
||||||
|
|
||||||
let content = make_quest_message_content(ctx, &quest);
|
let accounts = ctx.data().config.load_accounts();
|
||||||
|
let content = make_quest_message_content(ctx, &quest, &Some(accounts));
|
||||||
|
|
||||||
let builder = CreateMessage::new()
|
let builder = CreateMessage::new()
|
||||||
.content(content);
|
.content(content);
|
||||||
|
|
@ -327,8 +362,10 @@ pub async fn publish_inner(ctx: Context<'_>, quest: &mut Quest) -> Result<(), Er
|
||||||
guard.quests_channel
|
guard.quests_channel
|
||||||
};
|
};
|
||||||
|
|
||||||
channel.send_message(ctx, builder).await?;
|
match channel.send_message(ctx, builder).await {
|
||||||
Ok(())
|
Ok(m) => Ok(m),
|
||||||
|
Err(error) => Err(Error::SerenityError(error)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mark quest as public and send its message in quests channel
|
/// Mark quest as public and send its message in quests channel
|
||||||
|
|
@ -337,6 +374,7 @@ pub async fn publish_inner(ctx: Context<'_>, quest: &mut Quest) -> Result<(), Er
|
||||||
slash_command,
|
slash_command,
|
||||||
required_permissions = "ADMINISTRATOR",
|
required_permissions = "ADMINISTRATOR",
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
name_localized("ru", "опубликовать"),
|
name_localized("ru", "опубликовать"),
|
||||||
description_localized("ru", "Отметить квест как публичный и отправить его сообщение в канал квестов"),
|
description_localized("ru", "Отметить квест как публичный и отправить его сообщение в канал квестов"),
|
||||||
)]
|
)]
|
||||||
|
|
@ -357,10 +395,12 @@ pub async fn publish(
|
||||||
return Err(Error::QuestIsPublic(id));
|
return Err(Error::QuestIsPublic(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
publish_inner(ctx, quest).await?;
|
let message = publish_inner(ctx, quest).await?;
|
||||||
|
|
||||||
let strings = &ctx.data().strings;
|
let strings = &ctx.data().strings;
|
||||||
let formatter = strings.formatter().quest(&quest);
|
let formatter = strings.formatter()
|
||||||
|
.quest(&quest)
|
||||||
|
.message(&message);
|
||||||
|
|
||||||
let reply_string = formatter.fmt(&strings.quest.publish);
|
let reply_string = formatter.fmt(&strings.quest.publish);
|
||||||
ctx.reply(reply_string).await?;
|
ctx.reply(reply_string).await?;
|
||||||
|
|
@ -374,6 +414,7 @@ pub async fn publish(
|
||||||
slash_command,
|
slash_command,
|
||||||
required_permissions = "ADMINISTRATOR",
|
required_permissions = "ADMINISTRATOR",
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
name_localized("ru", "удалить"),
|
name_localized("ru", "удалить"),
|
||||||
description_localized("ru", "Удалить квест (и его сообщение, если он был опубликован)"),
|
description_localized("ru", "Удалить квест (и его сообщение, если он был опубликован)"),
|
||||||
)]
|
)]
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
use poise::serenity_prelude::{Attachment, ChannelId, CreateAttachment, CreateMessage, EditMessage, MessageId, UserId};
|
use poise::serenity_prelude::{Attachment, ChannelId, CreateAttachment, CreateMessage, EditMessage, MessageId, UserId};
|
||||||
|
|
||||||
use crate::{Context, Error};
|
use crate::{Context, Error, commands::guild};
|
||||||
|
|
||||||
#[poise::command(
|
#[poise::command(
|
||||||
prefix_command,
|
prefix_command,
|
||||||
slash_command,
|
slash_command,
|
||||||
required_permissions = "ADMINISTRATOR",
|
required_permissions = "ADMINISTRATOR",
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
subcommands("msg", "edit", "undo"),
|
subcommands("msg", "edit", "undo"),
|
||||||
name_localized("ru", "сообщение"),
|
name_localized("ru", "сообщение"),
|
||||||
)]
|
)]
|
||||||
|
|
@ -20,6 +21,7 @@ pub async fn social( _ctx: Context<'_> ) -> Result<(), Error> {
|
||||||
slash_command,
|
slash_command,
|
||||||
required_permissions = "ADMINISTRATOR",
|
required_permissions = "ADMINISTRATOR",
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
name_localized("ru", "написать"),
|
name_localized("ru", "написать"),
|
||||||
description_localized("ru", "Отправить сообщение пользователю или в канал"),
|
description_localized("ru", "Отправить сообщение пользователю или в канал"),
|
||||||
)]
|
)]
|
||||||
|
|
@ -98,6 +100,7 @@ pub async fn msg (
|
||||||
slash_command,
|
slash_command,
|
||||||
required_permissions = "ADMINISTRATOR",
|
required_permissions = "ADMINISTRATOR",
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
name_localized("ru", "редактировать"),
|
name_localized("ru", "редактировать"),
|
||||||
description_localized("ru", "Редактировать сообщение в канале или в ЛС"),
|
description_localized("ru", "Редактировать сообщение в канале или в ЛС"),
|
||||||
)]
|
)]
|
||||||
|
|
@ -177,6 +180,7 @@ pub async fn edit (
|
||||||
slash_command,
|
slash_command,
|
||||||
required_permissions = "ADMINISTRATOR",
|
required_permissions = "ADMINISTRATOR",
|
||||||
guild_only,
|
guild_only,
|
||||||
|
check = "guild",
|
||||||
name_localized("ru", "удалить"),
|
name_localized("ru", "удалить"),
|
||||||
description_localized("ru", "Удалить сообщение в канале или в ЛС"),
|
description_localized("ru", "Удалить сообщение в канале или в ЛС"),
|
||||||
)]
|
)]
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ use std::fmt::Display;
|
||||||
use poise::serenity_prelude as serenity;
|
use poise::serenity_prelude as serenity;
|
||||||
use squad_quest::error::MapError;
|
use squad_quest::error::MapError;
|
||||||
|
|
||||||
|
use crate::strings::ErrorStrings;
|
||||||
|
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
|
@ -21,6 +23,11 @@ pub enum Error {
|
||||||
RoomAlreadyUnlocked(u16),
|
RoomAlreadyUnlocked(u16),
|
||||||
CannotReach(u16),
|
CannotReach(u16),
|
||||||
TimerSet,
|
TimerSet,
|
||||||
|
NotThisGuild,
|
||||||
|
QuestLimitExceeded(u16),
|
||||||
|
BothUrlAndAttachment,
|
||||||
|
NoUrlOrAttachment,
|
||||||
|
NonImageAttachment,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<serenity::Error> for Error {
|
impl From<serenity::Error> for Error {
|
||||||
|
|
@ -53,9 +60,9 @@ impl Display for Error {
|
||||||
Self::QuestNotFound(u16) => write!(f, "quest #{u16} not found"),
|
Self::QuestNotFound(u16) => write!(f, "quest #{u16} not found"),
|
||||||
Self::QuestIsPublic(u16) => write!(f, "quest #{u16} is already public"),
|
Self::QuestIsPublic(u16) => write!(f, "quest #{u16} is already public"),
|
||||||
Self::QuestIsCompleted(u16) => write!(f, "quest #{u16} is already completed for this user"),
|
Self::QuestIsCompleted(u16) => write!(f, "quest #{u16} is already completed for this user"),
|
||||||
Self::NoContent => write!(f, "no text or attachment was specified"),
|
Self::NoContent => write!(f, "no text or attachment were specified"),
|
||||||
Self::NoChannelOrUser => write!(f, "no channel or user was specified"),
|
Self::NoChannelOrUser => write!(f, "no channel or user were specified"),
|
||||||
Self::BothChannelAndUser => write!(f, "both channel and user was specified"),
|
Self::BothChannelAndUser => write!(f, "both channel and user were specified"),
|
||||||
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"),
|
||||||
|
|
@ -65,6 +72,11 @@ impl Display for Error {
|
||||||
Self::RoomAlreadyUnlocked(id) => write!(f, "room #{id} is already unlocked for this user"),
|
Self::RoomAlreadyUnlocked(id) => write!(f, "room #{id} is already unlocked for this user"),
|
||||||
Self::CannotReach(id) => write!(f, "user cannot reach room #{id}"),
|
Self::CannotReach(id) => write!(f, "user cannot reach room #{id}"),
|
||||||
Self::TimerSet => write!(f, "timer is already set"),
|
Self::TimerSet => write!(f, "timer is already set"),
|
||||||
|
Self::NotThisGuild => write!(f, "cannot be used in this guild"),
|
||||||
|
Self::QuestLimitExceeded(id) => write!(f, "exceeded limit for quest #{id}"),
|
||||||
|
Self::BothUrlAndAttachment => write!(f, "both url and attachment were specified"),
|
||||||
|
Self::NoUrlOrAttachment => write!(f, "no url or attachment were specified"),
|
||||||
|
Self::NonImageAttachment => write!(f, "attachment is not an image"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -84,11 +96,41 @@ impl std::error::Error for Error {
|
||||||
Self::RoomNotFound(_) |
|
Self::RoomNotFound(_) |
|
||||||
Self::RoomAlreadyUnlocked(_) |
|
Self::RoomAlreadyUnlocked(_) |
|
||||||
Self::CannotReach(_) |
|
Self::CannotReach(_) |
|
||||||
Self::TimerSet => None,
|
Self::TimerSet |
|
||||||
|
Self::NotThisGuild |
|
||||||
|
Self::QuestLimitExceeded(_) |
|
||||||
|
Self::BothUrlAndAttachment |
|
||||||
|
Self::NoUrlOrAttachment |
|
||||||
|
Self::NonImageAttachment => None,
|
||||||
Self::SerenityError(error) => Some(error),
|
Self::SerenityError(error) => Some(error),
|
||||||
Self::SquadQuestError(error) => Some(error),
|
Self::SquadQuestError(error) => Some(error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> Error {
|
||||||
|
pub fn formattable_string(&self, errors: &'a ErrorStrings) -> &'a str {
|
||||||
|
match self {
|
||||||
|
Self::QuestNotFound(_) => &errors.quest_not_found,
|
||||||
|
Self::QuestIsPublic(_) => &errors.quest_is_public,
|
||||||
|
Self::QuestIsCompleted(_) => &errors.quest_is_completed,
|
||||||
|
Self::NoContent => &errors.no_content,
|
||||||
|
Self::NoChannelOrUser => &errors.no_channel_or_user,
|
||||||
|
Self::BothChannelAndUser => &errors.both_channel_and_user,
|
||||||
|
Self::SerenityError(_) => &errors.discord_error,
|
||||||
|
Self::SquadQuestError(_) => &errors.library_error,
|
||||||
|
Self::AccountNotFound => &errors.account_not_found,
|
||||||
|
Self::AccountIsSelf => &errors.account_is_self,
|
||||||
|
Self::InsufficientFunds(_) => &errors.insufficient_funds,
|
||||||
|
Self::RoomNotFound(_) => &errors.room_not_found,
|
||||||
|
Self::RoomAlreadyUnlocked(_) => &errors.room_already_unlocked,
|
||||||
|
Self::CannotReach(_) => &errors.cannot_reach,
|
||||||
|
Self::TimerSet => &errors.timer_set,
|
||||||
|
Self::NotThisGuild => &errors.not_this_guild,
|
||||||
|
Self::QuestLimitExceeded(_) => &errors.quest_limit_exceeded,
|
||||||
|
Self::BothUrlAndAttachment => &errors.both_url_and_attachment,
|
||||||
|
Self::NoUrlOrAttachment => &errors.no_url_or_attachment,
|
||||||
|
Self::NonImageAttachment => &errors.non_image_attachment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ async fn main() {
|
||||||
commands::account::reset(),
|
commands::account::reset(),
|
||||||
commands::map::unlock(),
|
commands::map::unlock(),
|
||||||
commands::map::r#move(),
|
commands::map::r#move(),
|
||||||
|
commands::map::avatar(),
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,20 @@ impl StringFormatter {
|
||||||
let name = ("{q.name}".to_string(), quest.name.clone());
|
let name = ("{q.name}".to_string(), quest.name.clone());
|
||||||
let description = ("{q.description}".to_string(), quest.description.clone());
|
let description = ("{q.description}".to_string(), quest.description.clone());
|
||||||
let answer = ("{q.answer}".to_string(), quest.answer.clone());
|
let answer = ("{q.answer}".to_string(), quest.answer.clone());
|
||||||
let new_tags = vec![ id, difficulty, reward, name, description, answer ];
|
let limit = ("{q.limit}".to_string(), quest.limit.to_string());
|
||||||
|
let new_tags = vec![ id, difficulty, reward, name, description, answer, limit ];
|
||||||
|
|
||||||
self.with_tags(new_tags)
|
self.with_tags(new_tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn quest_full(mut self, quest: &Quest, accounts: &Vec<Account>) -> Self {
|
||||||
|
self = self.quest(&quest);
|
||||||
|
let completed_times = accounts.iter().filter(|a| a.has_completed_quest(&quest)).count();
|
||||||
|
let completions = ("{q.completions}".to_string(), completed_times.to_string());
|
||||||
|
|
||||||
|
self.with_tags(vec![completions])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn user(self, user: &User) -> Self {
|
pub fn user(self, user: &User) -> Self {
|
||||||
let mention = ("{u.mention}".to_string(), user.mention().to_string());
|
let mention = ("{u.mention}".to_string(), user.mention().to_string());
|
||||||
let name = ("{u.name}".to_string(), user.display_name().to_string());
|
let name = ("{u.name}".to_string(), user.display_name().to_string());
|
||||||
|
|
@ -133,6 +142,7 @@ pub struct Strings {
|
||||||
pub scoreboard: Scoreboard,
|
pub scoreboard: Scoreboard,
|
||||||
pub social: Social,
|
pub social: Social,
|
||||||
pub quest: QuestStrings,
|
pub quest: QuestStrings,
|
||||||
|
pub error: ErrorStrings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Strings {
|
impl Default for Strings {
|
||||||
|
|
@ -151,6 +161,7 @@ impl Default for Strings {
|
||||||
social: Social::default(),
|
social: Social::default(),
|
||||||
account: AccountReplies::default(),
|
account: AccountReplies::default(),
|
||||||
map: MapReplies::default(),
|
map: MapReplies::default(),
|
||||||
|
error: ErrorStrings::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -328,7 +339,7 @@ impl Default for QuestStrings {
|
||||||
list_item: "{n}{q.id}: {q.name}{n} Description: {q.description}".to_string(),
|
list_item: "{n}{q.id}: {q.name}{n} Description: {q.description}".to_string(),
|
||||||
create: "Created quest {q.id}".to_string(),
|
create: "Created quest {q.id}".to_string(),
|
||||||
update: "Updated quest {q.id}".to_string(),
|
update: "Updated quest {q.id}".to_string(),
|
||||||
publish: "Published quest {q.id}: {text}".to_string(),
|
publish: "Published quest {q.id}: {m.link}".to_string(),
|
||||||
delete: "Deleted quest {q.id}".to_string(),
|
delete: "Deleted quest {q.id}".to_string(),
|
||||||
message_format: "### `{q.id}` {q.name} (+{q.reward}){n}\
|
message_format: "### `{q.id}` {q.name} (+{q.reward}){n}\
|
||||||
Difficulty: *{q.difficulty}*{n}\
|
Difficulty: *{q.difficulty}*{n}\
|
||||||
|
|
@ -382,6 +393,8 @@ impl Default for AccountReplies {
|
||||||
pub struct MapReplies {
|
pub struct MapReplies {
|
||||||
pub room_unlocked: String,
|
pub room_unlocked: String,
|
||||||
pub moved_to_room: String,
|
pub moved_to_room: String,
|
||||||
|
pub updated_avatar: String,
|
||||||
|
pub processing_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MapReplies {
|
impl Default for MapReplies {
|
||||||
|
|
@ -389,6 +402,62 @@ impl Default for MapReplies {
|
||||||
Self {
|
Self {
|
||||||
room_unlocked: "Unlocked room #{value}. Your balance: {b.current}".to_string(),
|
room_unlocked: "Unlocked room #{value}. Your balance: {b.current}".to_string(),
|
||||||
moved_to_room: "Moved to room #{value}".to_string(),
|
moved_to_room: "Moved to room #{value}".to_string(),
|
||||||
|
updated_avatar: "Successfully changed avatar".to_string(),
|
||||||
|
processing_url: "Processing URL...".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct ErrorStrings {
|
||||||
|
pub non_command_error: String,
|
||||||
|
pub quest_not_found: String,
|
||||||
|
pub quest_is_public: String,
|
||||||
|
pub quest_is_completed: String,
|
||||||
|
pub no_content: String,
|
||||||
|
pub no_channel_or_user: String,
|
||||||
|
pub both_channel_and_user: String,
|
||||||
|
pub discord_error: String,
|
||||||
|
pub library_error: String,
|
||||||
|
pub account_not_found: String,
|
||||||
|
pub account_is_self: String,
|
||||||
|
pub insufficient_funds: String,
|
||||||
|
pub room_not_found: String,
|
||||||
|
pub room_already_unlocked: String,
|
||||||
|
pub cannot_reach: String,
|
||||||
|
pub timer_set: String,
|
||||||
|
pub not_this_guild: String,
|
||||||
|
pub quest_limit_exceeded: String,
|
||||||
|
pub both_url_and_attachment: String,
|
||||||
|
pub no_url_or_attachment: String,
|
||||||
|
pub non_image_attachment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ErrorStrings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
non_command_error: "Internal server error: {text}".to_string(),
|
||||||
|
quest_not_found: "Quest {q.id} not found".to_string(),
|
||||||
|
quest_is_public: "Quest {q.id} is already public".to_string(),
|
||||||
|
quest_is_completed: "Quest {q.id} is already completed for this user".to_string(),
|
||||||
|
no_content: "No text or attachment were specified".to_string(),
|
||||||
|
no_channel_or_user: "No channel or user were specified".to_string(),
|
||||||
|
both_channel_and_user: "Both channel and user were specified".to_string(),
|
||||||
|
discord_error: "Discord interaction error: {text}".to_string(),
|
||||||
|
library_error: "Some internal logic error: {text}".to_string(),
|
||||||
|
account_not_found: "Given account was not found".to_string(),
|
||||||
|
account_is_self: "Given account is the same as command invoker".to_string(),
|
||||||
|
insufficient_funds: "You don't have {value} points".to_string(),
|
||||||
|
room_not_found: "Room #{value} not found".to_string(),
|
||||||
|
room_already_unlocked: "Room #{value} is already unlocked for this account".to_string(),
|
||||||
|
cannot_reach: "You cannot reach room #{value}".to_string(),
|
||||||
|
timer_set: "Timer is already set".to_string(),
|
||||||
|
not_this_guild: "Bot cannot be used in this guild".to_string(),
|
||||||
|
quest_limit_exceeded: "Exceeded limit for quest {q.id}".to_string(),
|
||||||
|
both_url_and_attachment: "Both URL and attachment were specified".to_string(),
|
||||||
|
no_url_or_attachment: "No URL or attachment were specified".to_string(),
|
||||||
|
non_image_attachment: "Given attachment is not an image".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use std::{collections::HashMap, fs, io::Write, path::PathBuf};
|
||||||
|
|
||||||
use serde::{ Serialize, Deserialize };
|
use serde::{ Serialize, Deserialize };
|
||||||
|
|
||||||
use crate::{SquadObject, error::Error};
|
use crate::{error::Error, quest::Quest, SquadObject};
|
||||||
|
|
||||||
fn default_id() -> String {
|
fn default_id() -> String {
|
||||||
"none".to_string()
|
"none".to_string()
|
||||||
|
|
@ -99,3 +99,10 @@ impl SquadObject for Account {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Account {
|
||||||
|
/// Returns true if given quest is completed on this account
|
||||||
|
pub fn has_completed_quest(&self, quest: &Quest) -> bool {
|
||||||
|
self.quests_completed.contains(&quest.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,18 @@ impl std::error::Error for Error {
|
||||||
pub enum QuestError {
|
pub enum QuestError {
|
||||||
/// Quest (self.0) is already completed for given account (self.1)
|
/// Quest (self.0) is already completed for given account (self.1)
|
||||||
AlreadyCompleted(u16, String),
|
AlreadyCompleted(u16, String),
|
||||||
|
/// Account (self.0) not found
|
||||||
|
AccountNotFound(String),
|
||||||
|
/// Limit for quest (self.0) exceeded
|
||||||
|
LimitExceeded(u16),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for QuestError {
|
impl fmt::Display for QuestError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::AlreadyCompleted(quest_id, account_id) => write!(f, "quest #{quest_id} is already completed for account \"{account_id}\""),
|
Self::AlreadyCompleted(quest_id, account_id) => write!(f, "quest #{quest_id} is already completed for account \"{account_id}\""),
|
||||||
|
Self::AccountNotFound(account_id) => write!(f, "account \"{account_id}\""),
|
||||||
|
Self::LimitExceeded(quest_id) => write!(f, "exceeded limit for quest #{quest_id}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,10 @@ pub struct Quest {
|
||||||
|
|
||||||
/// Additional implementation-defined data
|
/// Additional implementation-defined data
|
||||||
pub data: Option<HashMap<String, String>>,
|
pub data: Option<HashMap<String, String>>,
|
||||||
|
|
||||||
|
/// Limit how many users can complete the quest.
|
||||||
|
/// If set to 0, quest is not limited.
|
||||||
|
pub limit: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Quest {
|
impl Default for Quest {
|
||||||
|
|
@ -85,6 +89,7 @@ impl Default for Quest {
|
||||||
available_on: None,
|
available_on: None,
|
||||||
deadline: None,
|
deadline: None,
|
||||||
data: None,
|
data: None,
|
||||||
|
limit: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +164,17 @@ impl Quest {
|
||||||
/// // handle error
|
/// // handle error
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn complete_for_account(&self, account: &mut Account) -> Result<(),QuestError> {
|
pub fn complete_for_account(&self, id: &str, accounts: &mut Vec<Account>) -> Result<(),QuestError> {
|
||||||
|
let completed_times = accounts.iter().filter(|a| a.has_completed_quest(self)).count();
|
||||||
|
|
||||||
|
if self.limit > 0 && completed_times as u8 >= self.limit {
|
||||||
|
return Err(QuestError::LimitExceeded(self.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(account) = accounts.iter_mut().find(|a| a.id == id) else {
|
||||||
|
return Err(QuestError::AccountNotFound(id.to_string()));
|
||||||
|
};
|
||||||
|
|
||||||
match account.quests_completed.iter().find(|qid| **qid == self.id) {
|
match account.quests_completed.iter().find(|qid| **qid == self.id) {
|
||||||
Some(_) => Err(QuestError::AlreadyCompleted(self.id, account.id.clone())),
|
Some(_) => Err(QuestError::AlreadyCompleted(self.id, account.id.clone())),
|
||||||
None => {
|
None => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue