feat!: Added limit field to quests
- Bump version to 0.12.0 - lib: Changed Quest::complete_for_account behavior - cli: Added limit field for quest create and quest update - discord: Quests are checked for limit on /answer - discord: Added limit field for /quest create and /quest update - discord: Changed behavior of fetch_or_init_account
This commit is contained in:
parent
d188bba16e
commit
2640821a05
16 changed files with 192 additions and 69 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -2140,7 +2140,7 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
|||
|
||||
[[package]]
|
||||
name = "squad-quest"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml 0.9.10+spec-1.1.0",
|
||||
|
|
@ -2148,7 +2148,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "squad-quest-cli"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
|
|
@ -2159,7 +2159,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "squad-quest-discord"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
members = ["cli", "discord"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
edition = "2024"
|
||||
repository = "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"
|
||||
clap = { version = "4.5.53", 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"
|
||||
|
|
|
|||
|
|
@ -83,6 +83,9 @@ pub struct QuestCreateArgs {
|
|||
/// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24)
|
||||
#[arg(short,long,value_parser = parse_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)]
|
||||
|
|
@ -113,6 +116,9 @@ pub struct QuestUpdateArgs {
|
|||
/// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24)
|
||||
#[arg(long,value_parser = parse_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)]
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ fn main() {
|
|||
public: args.public,
|
||||
available_on: args.available.clone(),
|
||||
deadline: args.deadline.clone(),
|
||||
limit: args.limit.unwrap_or_default(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
|
@ -171,6 +172,7 @@ fn main() {
|
|||
public: args.public.unwrap_or(quest.public),
|
||||
available_on: args.available.clone().or(quest.available_on.clone()),
|
||||
deadline: args.deadline.clone().or(quest.deadline.clone()),
|
||||
limit: args.limit.unwrap_or_default(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
|
@ -284,10 +286,6 @@ fn main() {
|
|||
do_and_log(account.save(path), !cli.quiet, format!("Updated balance of account \"{}\".", account.id));
|
||||
},
|
||||
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();
|
||||
|
||||
|
|
@ -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}"),
|
||||
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"] }
|
||||
serde = "1.0.228"
|
||||
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"] }
|
||||
toml = "0.9.8"
|
||||
|
|
|
|||
|
|
@ -3,8 +3,14 @@ use std::collections::HashMap;
|
|||
use poise::serenity_prelude::{User, UserId};
|
||||
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();
|
||||
|
||||
if let Some(account) = accounts.iter().find(|a| a.id == id) {
|
||||
return Ok(account.clone());
|
||||
}
|
||||
|
||||
let mut data: HashMap<String, String> = HashMap::new();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
match accounts.iter().find(|a| a.id == id) {
|
||||
Some(a) => a.clone(),
|
||||
None => Account {
|
||||
id,
|
||||
let new_account = Account {
|
||||
id: id.to_string(),
|
||||
data: Some(data),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Err(new_account)
|
||||
}
|
||||
|
||||
pub fn account_rooms_value(account: &Account, map: &Map) -> u32 {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use poise::serenity_prelude::User;
|
||||
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, commands::guild};
|
||||
use crate::{Context, Error, account::{account_full_balance, account_user_id}, strings::StringFormatter, commands::guild};
|
||||
|
||||
async fn account_balance_string(
|
||||
ctx: &Context<'_>,
|
||||
|
|
@ -146,11 +146,18 @@ pub async fn give(
|
|||
let mut accounts = config.load_accounts();
|
||||
|
||||
let user_id = format!("{}", ctx.author().id.get());
|
||||
|
||||
let mut user_account = fetch_or_init_account(config, user_id, Some(ctx.author()));
|
||||
let strings = &ctx.data().strings;
|
||||
let formatter: StringFormatter;
|
||||
let accounts_path = config.full_accounts_path();
|
||||
|
||||
let who_id = format!("{}", who.id.get());
|
||||
let Some(other_account) = accounts.iter_mut().find(|a| a.id == who_id ) else {
|
||||
|
||||
if let None = accounts.iter().find(|a| a.id == who_id ) {
|
||||
return Err(Error::AccountNotFound);
|
||||
}
|
||||
|
||||
{
|
||||
let Some(user_account) = accounts.iter_mut().find(|a| a.id == user_id) else {
|
||||
return Err(Error::AccountNotFound);
|
||||
};
|
||||
|
||||
|
|
@ -159,17 +166,20 @@ pub async fn give(
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
let accounts_path = config.full_accounts_path();
|
||||
user_account.save(accounts_path.clone())?;
|
||||
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);
|
||||
ctx.reply(reply_string).await?;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use poise::serenity_prelude::{Attachment, ComponentInteractionCollector, CreateActionRow, CreateAttachment, CreateButton, CreateMessage, EditMessage};
|
||||
use squad_quest::SquadObject;
|
||||
|
||||
use crate::{Context, Error, account::fetch_or_init_account, commands::guild};
|
||||
use crate::{account::fetch_or_init_account, commands::{guild, quest::update_quest_message}, Context, Error};
|
||||
|
||||
/// Send an answer to the quest for review
|
||||
#[poise::command(
|
||||
|
|
@ -35,18 +35,33 @@ pub async fn answer(
|
|||
#[description_localized("ru", "Вложение к ответу на квест")]
|
||||
file3: Option<Attachment>,
|
||||
) -> 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 Some(quest) = quests.iter()
|
||||
.filter(|q| q.public)
|
||||
.find(|q| q.id == quest_id) else {
|
||||
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();
|
||||
for file in [file1, file2, file3] {
|
||||
|
|
@ -135,16 +150,20 @@ pub async fn answer(
|
|||
let content: String;
|
||||
if is_approved {
|
||||
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}");
|
||||
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();
|
||||
if let Err(error) = account.save(path) {
|
||||
eprintln!("{error}");
|
||||
no_errors = false;
|
||||
};
|
||||
|
||||
update_quest_message(ctx, &quest).await?;
|
||||
|
||||
formatter = formatter.current_balance(&account);
|
||||
|
||||
if no_errors {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@ pub async fn unlock(
|
|||
};
|
||||
|
||||
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 {
|
||||
return Err(Error::InsufficientFunds(room.value));
|
||||
|
|
@ -70,7 +73,10 @@ pub async fn r#move(
|
|||
let conf = &ctx.data().config;
|
||||
|
||||
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) {
|
||||
return Err(Error::CannotReach(id));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
use std::{future, str::FromStr};
|
||||
|
||||
use poise::serenity_prelude::{CreateMessage, EditMessage, Message, futures::StreamExt};
|
||||
use squad_quest::{SquadObject, quest::{Quest, QuestDifficulty}};
|
||||
use poise::serenity_prelude as serenity;
|
||||
use serenity::{CreateMessage, EditMessage, Message, futures::StreamExt};
|
||||
use squad_quest::{account::Account, quest::{Quest, QuestDifficulty}, SquadObject};
|
||||
use toml::value::Date;
|
||||
use crate::{Context, Error,commands::guild};
|
||||
|
||||
|
|
@ -24,12 +25,37 @@ async fn find_quest_message(ctx: Context<'_>, id: u16) -> Result<Option<Message>
|
|||
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 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)
|
||||
}
|
||||
|
||||
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(
|
||||
prefix_command,
|
||||
slash_command,
|
||||
|
|
@ -146,6 +172,10 @@ pub async fn create(
|
|||
#[name_localized("ru", "доступен")]
|
||||
#[description_localized("ru", "Дата публикации (в формате ГГГГ-ММ-ДД, напр. 2025-12-24)")]
|
||||
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)"]
|
||||
#[name_localized("ru", "дедлайн")]
|
||||
|
|
@ -182,6 +212,7 @@ pub async fn create(
|
|||
answer,
|
||||
public: false,
|
||||
available_on,
|
||||
limit: limit.unwrap_or_default(),
|
||||
//deadline,
|
||||
..Default::default()
|
||||
};
|
||||
|
|
@ -239,6 +270,10 @@ pub async fn update(
|
|||
#[name_localized("ru", "доступен")]
|
||||
#[description_localized("ru", "Дата публикации (в формате ГГГГ-ММ-ДД, напр. 2025-12-24")]
|
||||
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)"]
|
||||
#[name_localized("ru", "дедлайн")]
|
||||
|
|
@ -264,15 +299,18 @@ pub async fn update(
|
|||
};
|
||||
|
||||
let available_on: Option<Date>;
|
||||
let new_limit: u8;
|
||||
//let dead_line: Option<Date>;
|
||||
|
||||
match reset.unwrap_or(false) {
|
||||
true => {
|
||||
available_on = None;
|
||||
new_limit = limit.unwrap_or_default();
|
||||
//dead_line = None;
|
||||
},
|
||||
false => {
|
||||
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()));
|
||||
},
|
||||
}
|
||||
|
|
@ -286,6 +324,7 @@ pub async fn update(
|
|||
answer: answer.unwrap_or(quest.answer.clone()),
|
||||
public: quest.public,
|
||||
available_on,
|
||||
limit: new_limit,
|
||||
//deadline: dead_line,
|
||||
..Default::default()
|
||||
};
|
||||
|
|
@ -294,16 +333,7 @@ pub async fn update(
|
|||
let formatter = strings.formatter().quest(&new_quest);
|
||||
|
||||
if new_quest.public {
|
||||
let content = make_quest_message_content(ctx, &new_quest);
|
||||
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?;
|
||||
}
|
||||
update_quest_message(ctx, &new_quest).await?;
|
||||
}
|
||||
|
||||
let path = conf.full_quests_path();
|
||||
|
|
@ -320,7 +350,8 @@ pub async fn publish_inner(ctx: Context<'_>, quest: &mut Quest) -> Result<Messag
|
|||
let quests_path = ctx.data().config.full_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()
|
||||
.content(content);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ pub enum Error {
|
|||
CannotReach(u16),
|
||||
TimerSet,
|
||||
NotThisGuild,
|
||||
QuestLimitExceeded(u16),
|
||||
}
|
||||
|
||||
impl From<serenity::Error> for Error {
|
||||
|
|
@ -67,6 +68,7 @@ impl Display for Error {
|
|||
Self::CannotReach(id) => write!(f, "user cannot reach room #{id}"),
|
||||
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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -87,7 +89,8 @@ impl std::error::Error for Error {
|
|||
Self::RoomAlreadyUnlocked(_) |
|
||||
Self::CannotReach(_) |
|
||||
Self::TimerSet |
|
||||
Self::NotThisGuild => None,
|
||||
Self::NotThisGuild |
|
||||
Self::QuestLimitExceeded(_) => None,
|
||||
Self::SerenityError(error) => Some(error),
|
||||
Self::SquadQuestError(error) => Some(error),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,11 +45,20 @@ impl StringFormatter {
|
|||
let name = ("{q.name}".to_string(), quest.name.clone());
|
||||
let description = ("{q.description}".to_string(), quest.description.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)
|
||||
}
|
||||
|
||||
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 {
|
||||
let mention = ("{u.mention}".to_string(), user.mention().to_string());
|
||||
let name = ("{u.name}".to_string(), user.display_name().to_string());
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::{collections::HashMap, fs, io::Write, path::PathBuf};
|
|||
|
||||
use serde::{ Serialize, Deserialize };
|
||||
|
||||
use crate::{SquadObject, error::Error};
|
||||
use crate::{error::Error, quest::Quest, SquadObject};
|
||||
|
||||
fn default_id() -> String {
|
||||
"none".to_string()
|
||||
|
|
@ -99,3 +99,10 @@ impl SquadObject for Account {
|
|||
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 {
|
||||
/// Quest (self.0) is already completed for given account (self.1)
|
||||
AlreadyCompleted(u16, String),
|
||||
/// Account (self.0) not found
|
||||
AccountNotFound(String),
|
||||
/// Limit for quest (self.0) exceeded
|
||||
LimitExceeded(u16),
|
||||
}
|
||||
|
||||
impl fmt::Display for QuestError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
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
|
||||
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 {
|
||||
|
|
@ -85,6 +89,7 @@ impl Default for Quest {
|
|||
available_on: None,
|
||||
deadline: None,
|
||||
data: None,
|
||||
limit: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -159,7 +164,17 @@ impl Quest {
|
|||
/// // 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) {
|
||||
Some(_) => Err(QuestError::AlreadyCompleted(self.id, account.id.clone())),
|
||||
None => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue