updated to experimental in development version of serenity and component V2 messages
This commit is contained in:
parent
4a4f3671e7
commit
0ebe9e3be3
4 changed files with 649 additions and 479 deletions
16
src/dice.rs
16
src/dice.rs
|
|
@ -33,20 +33,6 @@ pub struct DiceFormula {
|
|||
pub(crate) x: bool,
|
||||
}
|
||||
|
||||
impl DiceFormula {
|
||||
pub fn new(size: AstNodeId) -> Self {
|
||||
Self {
|
||||
count: None,
|
||||
size,
|
||||
kh: None,
|
||||
kl: None,
|
||||
dh: None,
|
||||
dl: None,
|
||||
x: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Expression {
|
||||
pub fn evaluate(&self, mut rng: impl Rng) -> anyhow::Result<String> {
|
||||
let mut result = String::new();
|
||||
|
|
@ -59,7 +45,7 @@ impl Expression {
|
|||
return Ok(avg);
|
||||
}
|
||||
let mut average = self.node_sample(rng, self.root)?;
|
||||
for idx in 1..1000 {
|
||||
for idx in 1..3000 {
|
||||
average =
|
||||
(average * idx as f64 + self.node_sample(rng, self.root)?) / (idx as f64 + 1f64);
|
||||
}
|
||||
|
|
|
|||
240
src/main.rs
240
src/main.rs
|
|
@ -1,94 +1,208 @@
|
|||
use std::fmt::Write;
|
||||
mod dice;
|
||||
mod parsing;
|
||||
|
||||
use std::env;
|
||||
|
||||
use anyhow::bail;
|
||||
use dotenv::dotenv;
|
||||
use poise::CreateReply;
|
||||
use serenity::{client::ClientBuilder, model::prelude::*};
|
||||
use serenity::builder::CreateEmbed;
|
||||
|
||||
type Context<'a> = poise::Context<'a, Data, anyhow::Error>;
|
||||
pub struct Data;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use serenity::all::{
|
||||
Command, CommandInteraction, CommandOptionType, CreateCommandOption, CreateComponent, FullEvent, Interaction, MessageFlags, ResolvedOption, ResolvedValue,
|
||||
};
|
||||
use serenity::builder::{
|
||||
CreateCommand, CreateContainer, CreateContainerComponent, CreateInteractionResponse,
|
||||
CreateInteractionResponseFollowup, CreateInteractionResponseMessage, CreateTextDisplay,
|
||||
};
|
||||
use serenity::{async_trait, prelude::*};
|
||||
use std::sync::Arc;
|
||||
use tracing::{info, warn};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
dotenv().ok();
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
|
||||
// Configure the client with your Discord bot token in the environment.
|
||||
let token =
|
||||
Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment");
|
||||
|
||||
let intents = GatewayIntents::non_privileged();
|
||||
// Build our client.
|
||||
let mut client = Client::builder(token, GatewayIntents::empty())
|
||||
.event_handler(Arc::new(Handler))
|
||||
.await
|
||||
.expect("Error creating client");
|
||||
|
||||
let framework = poise::Framework::builder()
|
||||
.options(poise::FrameworkOptions {
|
||||
commands: vec![help(), roll()],
|
||||
..Default::default()
|
||||
})
|
||||
.setup(move |ctx, _ready, framework| {
|
||||
Box::pin(async move {
|
||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||
Ok(Data {})
|
||||
})
|
||||
})
|
||||
.build();
|
||||
|
||||
let client = ClientBuilder::new(token, intents)
|
||||
.framework(framework)
|
||||
.await;
|
||||
|
||||
client.unwrap().start().await?;
|
||||
client.start().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn help(ctx: Context<'_>, command: Option<String>) -> anyhow::Result<()> {
|
||||
let configuration = poise::builtins::HelpConfiguration {
|
||||
..Default::default()
|
||||
struct Handler;
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for Handler {
|
||||
async fn dispatch(&self, ctx: &Context, event: &FullEvent) {
|
||||
// clippy can't decide between if it wants it collapsed, or if it wants you to use if let
|
||||
// because it's a single pattern.
|
||||
#[expect(clippy::collapsible_match)]
|
||||
match event {
|
||||
FullEvent::InteractionCreate { interaction, .. } => {
|
||||
if let Interaction::Command(command) = interaction {
|
||||
info!("Received command interaction: {command:#?}");
|
||||
|
||||
let content = match command.data.name.as_str() {
|
||||
"roll" => {
|
||||
roll(ctx, command)
|
||||
.await
|
||||
.unwrap_or_else(|e| warn!("error! {}", e));
|
||||
None
|
||||
}
|
||||
_ => Some("not implemented :(".to_string()),
|
||||
};
|
||||
|
||||
if let Some(content) = content {
|
||||
let data = CreateInteractionResponseMessage::new().content(content);
|
||||
let builder = CreateInteractionResponse::Message(data);
|
||||
if let Err(why) = command.create_response(&ctx.http, builder).await {
|
||||
info!("Cannot respond to slash command: {why}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FullEvent::Ready { data_about_bot, .. } => {
|
||||
info!("{} is connected!", data_about_bot.user.name);
|
||||
|
||||
let global_command = Command::create_global_command(
|
||||
&ctx.http,
|
||||
CreateCommand::new("roll")
|
||||
.description("Roll some dice!")
|
||||
.add_option(CreateCommandOption::new(
|
||||
CommandOptionType::String,
|
||||
"formula",
|
||||
"The dice formula, something like 2d20kh1+3",
|
||||
).required(true))
|
||||
.add_option(CreateCommandOption::new(
|
||||
CommandOptionType::Integer,
|
||||
"multi",
|
||||
"Repeat the rolled formula this many times. Useful if you're controlling too many units!",
|
||||
))
|
||||
.add_option(CreateCommandOption::new(
|
||||
CommandOptionType::String,
|
||||
"description",
|
||||
"What is that roll for? Add some context!",
|
||||
))
|
||||
.add_option(CreateCommandOption::new(
|
||||
CommandOptionType::Boolean,
|
||||
"private",
|
||||
"Keep the roll private and hidden, only for yourself.",
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
|
||||
info!("I created the following global slash command: {global_command:#?}");
|
||||
|
||||
info!("{} is connected!", data_about_bot.user.name);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn roll(ctx: &Context, command: &CommandInteraction) -> anyhow::Result<()> {
|
||||
let options = command.data.options();
|
||||
let Some(ResolvedOption {
|
||||
value: ResolvedValue::String(formula),
|
||||
..
|
||||
}) = options.iter().find(|option| option.name == "formula")
|
||||
else {
|
||||
bail!("Formula was not provided.")
|
||||
};
|
||||
poise::builtins::help(ctx, command.as_deref(), configuration).await?;
|
||||
Ok(())
|
||||
}
|
||||
let multi = options
|
||||
.iter()
|
||||
.find(|option| option.name == "multi")
|
||||
.and_then(|option| {
|
||||
if let ResolvedValue::Integer(multi) = option.value {
|
||||
Some(multi)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(1)
|
||||
.min(20);
|
||||
let private = options
|
||||
.iter()
|
||||
.find(|option| option.name == "private")
|
||||
.and_then(|option| {
|
||||
if let ResolvedValue::Boolean(private) = option.value {
|
||||
Some(private)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let description = options
|
||||
.iter()
|
||||
.find(|option| option.name == "description")
|
||||
.and_then(|option| {
|
||||
if let ResolvedValue::String(description) = option.value {
|
||||
Some(description)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn roll(
|
||||
ctx: Context<'_>,
|
||||
formula: String,
|
||||
#[description = "Roll the formula several times"] multi: Option<u32>,
|
||||
#[description = "Make the roll visible only to you"]
|
||||
#[flag]
|
||||
private: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let result = parsing::parse(&formula);
|
||||
let expression = match result {
|
||||
Ok(expression) => expression,
|
||||
Err(error) => {
|
||||
ctx.send(
|
||||
CreateReply::default()
|
||||
.content(format!("Dice formula could not be parsed.\n{}", error))
|
||||
.ephemeral(true)
|
||||
.reply(true),
|
||||
)
|
||||
.await?;
|
||||
command
|
||||
.create_response(
|
||||
&ctx.http,
|
||||
CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::default()
|
||||
.content(format!("Dice formula could not be parsed.\n{}", error))
|
||||
.ephemeral(true),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
if private {
|
||||
ctx.defer_ephemeral().await?;
|
||||
command.defer_ephemeral(&ctx.http).await?;
|
||||
} else {
|
||||
ctx.defer().await?;
|
||||
command.defer(&ctx.http).await?;
|
||||
}
|
||||
let multi = multi.unwrap_or(1).min(32);
|
||||
let mut content = format!("**{} rolled:**\n", ctx.author().mention());
|
||||
|
||||
let seed = rand::rng().next_u64();
|
||||
let mut rng = rand_xoshiro::Xoshiro256PlusPlus::seed_from_u64(seed);
|
||||
|
||||
let ev = (expression.average(&mut rng)? * 10.0).round() / 10.0;
|
||||
|
||||
let mut text_components = Vec::with_capacity((multi + 2) as usize);
|
||||
text_components.push(CreateContainerComponent::TextDisplay(
|
||||
CreateTextDisplay::new(if let Some(description) = description {
|
||||
format!("### {} rolled: *{description}*\n", command.user.mention())
|
||||
} else {
|
||||
format!("### {} rolled\n", command.user.mention())
|
||||
}),
|
||||
));
|
||||
for _ in 0..multi {
|
||||
write!(content, "{}\n", expression.evaluate(rand::rng())?)?;
|
||||
text_components.push(CreateContainerComponent::TextDisplay(
|
||||
CreateTextDisplay::new(format!("{}\n", expression.evaluate(&mut rng)?)),
|
||||
));
|
||||
}
|
||||
write!(content, "-# Expected Value: {}", (expression.average(&mut rand::rng())? * 10.0).round() / 10.0)?;
|
||||
let reply = CreateReply::default()
|
||||
.content(content)
|
||||
.ephemeral(private)
|
||||
.reply(true);
|
||||
ctx.send(reply).await?;
|
||||
text_components.push(CreateContainerComponent::TextDisplay(
|
||||
CreateTextDisplay::new(format!("-# Expected value: {ev} | Seed: {seed}")),
|
||||
));
|
||||
|
||||
command
|
||||
.create_followup(
|
||||
&ctx.http,
|
||||
CreateInteractionResponseFollowup::new()
|
||||
.components(vec![CreateComponent::Container(CreateContainer::new(
|
||||
text_components,
|
||||
))])
|
||||
.ephemeral(private)
|
||||
.flags(MessageFlags::IS_COMPONENTS_V2 | MessageFlags::SUPPRESS_NOTIFICATIONS),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue