Added basic role handling
+ Games now have a config for which roles they have + Roles are assigned at game start + Roles don't function within the bot, except the spy + Added a spy listener + Clippy + fmtmain
parent
7af3bab486
commit
275f5c9305
|
@ -1,6 +1,7 @@
|
|||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::game::role::Role;
|
||||
use config::{Config, File};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use structopt::StructOpt;
|
||||
|
@ -21,6 +22,7 @@ pub struct GameConfig {
|
|||
pub player_group_name: String,
|
||||
pub profile_album_hash: String,
|
||||
pub whispers_allowed: bool,
|
||||
pub roles: Vec<Role>,
|
||||
pub first_name: Vec<String>,
|
||||
pub last_name: Vec<String>,
|
||||
pub messages: MessageConfig,
|
||||
|
|
|
@ -21,6 +21,7 @@ use crate::game::message_router::{
|
|||
dispatch_message, Median, MessageDest, MessageSource, WoxlfMessage,
|
||||
};
|
||||
use crate::game::player_data::PlayerData;
|
||||
use crate::game::role::Role;
|
||||
use crate::game::Phase;
|
||||
use crate::messages::DiscordUser;
|
||||
|
||||
|
@ -93,10 +94,15 @@ async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||
|
||||
profile_pics.shuffle(&mut thread_rng());
|
||||
|
||||
let mut roles = global_data.game_cfg()?.roles.clone();
|
||||
|
||||
roles.shuffle(&mut thread_rng());
|
||||
|
||||
for player in players {
|
||||
let first_name = first_names.pop();
|
||||
let last_name = last_names.pop();
|
||||
let profile_pic_url = profile_pics.pop();
|
||||
let role = roles.pop();
|
||||
add_user_to_game(
|
||||
ctx,
|
||||
&guild,
|
||||
|
@ -105,6 +111,7 @@ async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||
first_name,
|
||||
last_name,
|
||||
profile_pic_url,
|
||||
role,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
@ -369,6 +376,7 @@ async fn test_theme(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|||
profile_pic_url: "".to_string(),
|
||||
channel_webhook_id: 0,
|
||||
alive: true,
|
||||
role: Role::Villager,
|
||||
};
|
||||
|
||||
let player_1_discord = DiscordUser {
|
||||
|
@ -384,6 +392,7 @@ async fn test_theme(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|||
profile_pic_url: "".to_string(),
|
||||
channel_webhook_id: 0,
|
||||
alive: false,
|
||||
role: Role::Villager,
|
||||
};
|
||||
|
||||
let mut players = vec![test_player_1.clone(), test_player_2.clone()];
|
||||
|
@ -623,7 +632,12 @@ async fn players(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
|||
if msg.channel_id.0 == global_data.cfg.discord_config.host_channel {
|
||||
let guild = msg.guild(&ctx.cache).unwrap();
|
||||
let member = guild.members.get(&UserId::from(player.discord_id)).unwrap();
|
||||
msg_builder.push_line(format!(" ({})", member.display_name()));
|
||||
msg_builder.push_line(format!(
|
||||
" ({}) [{} {}]",
|
||||
member.display_name(),
|
||||
player.role,
|
||||
player.role.seer_color()
|
||||
));
|
||||
} else {
|
||||
msg_builder.push_line("");
|
||||
}
|
||||
|
@ -638,14 +652,20 @@ async fn players(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
|||
#[aliases("pm", "w")]
|
||||
#[description = "Send a private message to another player."]
|
||||
async fn whisper(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
if args.len() < 2 {
|
||||
msg.reply(&ctx.http, "Need a recipient and message!")
|
||||
.await?;
|
||||
} else {
|
||||
let data = ctx.data.read().await;
|
||||
let global_data = data.get::<GlobalData>().unwrap();
|
||||
let mut global_data = global_data.lock().await;
|
||||
|
||||
if !global_data.game_cfg()?.whispers_allowed {
|
||||
msg.reply(&ctx.http, "No private messages are allowed in this game")
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if args.len() < 2 {
|
||||
msg.reply(&ctx.http, "Need a recipient and message!")
|
||||
.await?;
|
||||
} else {
|
||||
let target = args.single::<String>()?;
|
||||
let pm = args.rest();
|
||||
|
||||
|
|
|
@ -10,8 +10,10 @@ use crate::error::WoxlfError;
|
|||
use crate::game::game_state::PhaseDuration;
|
||||
use crate::game::global_data::GlobalData;
|
||||
use crate::game::player_data::PlayerData;
|
||||
use crate::game::role::Role;
|
||||
use crate::imgur::Image;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn add_user_to_game(
|
||||
ctx: &Context,
|
||||
guild: &Guild,
|
||||
|
@ -20,6 +22,7 @@ pub async fn add_user_to_game(
|
|||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
profile_pic: Option<Image>,
|
||||
role: Option<Role>,
|
||||
) -> error::Result<PlayerData> {
|
||||
if first_name.is_none() && last_name.is_none() {
|
||||
return Err(WoxlfError::RanOutOfCodenames);
|
||||
|
@ -29,6 +32,10 @@ pub async fn add_user_to_game(
|
|||
return Err(WoxlfError::RanOutOfProfilePics);
|
||||
}
|
||||
|
||||
if role.is_none() {
|
||||
return Err(WoxlfError::RanOutOfRoles);
|
||||
}
|
||||
|
||||
let codename = global_data
|
||||
.templates()?
|
||||
.build_name(global_data, first_name, last_name)
|
||||
|
@ -67,6 +74,7 @@ pub async fn add_user_to_game(
|
|||
channel_webhook_id: webhook.id.0,
|
||||
profile_pic_url: profile_pic.unwrap().link,
|
||||
alive: true,
|
||||
role: role.unwrap(),
|
||||
};
|
||||
|
||||
global_data
|
||||
|
|
|
@ -20,6 +20,8 @@ pub enum WoxlfError {
|
|||
TemplateError(tera::Error),
|
||||
RanOutOfProfilePics,
|
||||
UnsupportedMsgMedium,
|
||||
RanOutOfRoles,
|
||||
ConfigNotfound,
|
||||
}
|
||||
|
||||
impl std::error::Error for WoxlfError {}
|
||||
|
@ -45,6 +47,8 @@ impl Display for WoxlfError {
|
|||
WoxlfError::UnsupportedMsgMedium => {
|
||||
"Tried to send a message over an unsupported medium".to_string()
|
||||
}
|
||||
WoxlfError::RanOutOfRoles => "Ran out of user roles".to_string(),
|
||||
WoxlfError::ConfigNotfound => "Config not found".to_string(),
|
||||
};
|
||||
|
||||
write!(f, "Woxlf Error: {}", msg)
|
||||
|
|
|
@ -36,7 +36,10 @@ impl GlobalData {
|
|||
starting_phase: Phase,
|
||||
starting_phase_duration: Duration,
|
||||
) -> Result<()> {
|
||||
let game_config = self.cfg.get_game_config(game_name).unwrap();
|
||||
let game_config = self
|
||||
.cfg
|
||||
.get_game_config(game_name)
|
||||
.ok_or(WoxlfError::ConfigNotfound)?;
|
||||
|
||||
self.templates = Some(MessageTemplates::try_from(game_config.messages.clone())?);
|
||||
self.game_cfg = Some(game_config);
|
||||
|
|
|
@ -83,8 +83,8 @@ pub enum Priority {
|
|||
}
|
||||
|
||||
pub struct ListenerContext<'a> {
|
||||
data: &'a GlobalData,
|
||||
ctx: &'a Context,
|
||||
pub data: &'a mut GlobalData,
|
||||
pub ctx: &'a Context,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
|
|
@ -38,7 +38,7 @@ impl Default for MessageDest {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
pub enum Median {
|
||||
DirectMessage,
|
||||
|
@ -133,7 +133,7 @@ fn filter_source_channel(player_data: &&PlayerData, msg_source: &MessageSource)
|
|||
true
|
||||
}
|
||||
|
||||
async fn send_webhook_msg(
|
||||
pub async fn send_webhook_msg(
|
||||
http: &Http,
|
||||
webhook_id: WebhookId,
|
||||
username: &str,
|
||||
|
@ -162,23 +162,17 @@ async fn send_webhook_msg(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_private_message(
|
||||
pub async fn send_private_message(
|
||||
http: &Http,
|
||||
src_username: &str,
|
||||
dest: UserId,
|
||||
msg: &str,
|
||||
msg_content: &str,
|
||||
attachments: &Option<Vec<AttachmentType<'_>>>,
|
||||
) -> error::Result<()> {
|
||||
let dest_user = dest.to_user(http).await?;
|
||||
|
||||
let mut dm_message = MessageBuilder::new();
|
||||
|
||||
dm_message.push_bold_line_safe(format!("{} has sent you a DM:", src_username));
|
||||
dm_message.push(msg);
|
||||
|
||||
dest_user
|
||||
.dm(http, |msg| {
|
||||
msg.content(dm_message);
|
||||
msg.content(msg_content);
|
||||
|
||||
if let Some(attachments) = attachments {
|
||||
msg.add_files(attachments.clone());
|
||||
|
@ -265,11 +259,15 @@ pub async fn send_message(
|
|||
.await?;
|
||||
}
|
||||
Median::DirectMessage => {
|
||||
let dm_msg = MessageBuilder::new()
|
||||
.push_bold_line_safe(format!("{} has sent you a private message:", &msg.get_message_username(global_data)?))
|
||||
.push(&msg.content)
|
||||
.build();
|
||||
|
||||
send_private_message(
|
||||
&ctx.http,
|
||||
&msg.get_message_username(global_data)?,
|
||||
UserId(dest_player.discord_id),
|
||||
&msg.content,
|
||||
&dm_msg,
|
||||
&msg.attachments,
|
||||
)
|
||||
.await?;
|
||||
|
@ -283,6 +281,8 @@ pub async fn send_message(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a message to the proper channels
|
||||
/// Note safe to use in an event handler
|
||||
pub async fn dispatch_message(
|
||||
ctx: &Context,
|
||||
global_data: &mut GlobalData,
|
||||
|
|
|
@ -5,6 +5,7 @@ pub mod global_data;
|
|||
pub mod listener;
|
||||
pub mod message_router;
|
||||
pub mod player_data;
|
||||
pub mod role;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, Copy)]
|
||||
pub enum Phase {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::game::role::Role;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
|
@ -9,6 +10,7 @@ pub struct PlayerData {
|
|||
pub profile_pic_url: String,
|
||||
pub channel_webhook_id: u64,
|
||||
pub alive: bool,
|
||||
pub role: Role,
|
||||
}
|
||||
|
||||
impl PlayerData {
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use crate::game::listener::Listeners;
|
||||
use crate::game::role::spy::SpyListener;
|
||||
|
||||
mod spy;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
|
||||
pub enum RoleColor {
|
||||
Green,
|
||||
Red,
|
||||
Blue,
|
||||
Purple,
|
||||
}
|
||||
|
||||
impl Display for RoleColor {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
RoleColor::Green => "Green",
|
||||
RoleColor::Red => "Red",
|
||||
RoleColor::Blue => "Blue",
|
||||
RoleColor::Purple => "Purple",
|
||||
};
|
||||
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]
|
||||
pub enum Role {
|
||||
// Human Roles
|
||||
Villager,
|
||||
Seer,
|
||||
Guardian,
|
||||
Jailer,
|
||||
Coroner,
|
||||
Vigilante,
|
||||
Hunter,
|
||||
Psychic,
|
||||
Miller,
|
||||
Herring,
|
||||
Spy,
|
||||
Jester,
|
||||
Mason,
|
||||
Shepard,
|
||||
// Wolf Roles
|
||||
MasterWolf,
|
||||
WolfShaman,
|
||||
Wolf,
|
||||
// Custom Role
|
||||
Custom(String, RoleColor),
|
||||
}
|
||||
|
||||
impl Display for Role {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
Role::Villager => "Villager",
|
||||
Role::Seer => "Seer",
|
||||
Role::Guardian => "Guardian",
|
||||
Role::Jailer => "Jailer",
|
||||
Role::Coroner => "Coroner",
|
||||
Role::Vigilante => "Vigilante",
|
||||
Role::Hunter => "Hunter",
|
||||
Role::Psychic => "Psychic",
|
||||
Role::Miller => "Miller",
|
||||
Role::Herring => "Herring",
|
||||
Role::Spy => "Spy",
|
||||
Role::Jester => "Jester",
|
||||
Role::Mason => "Mason",
|
||||
Role::Shepard => "Shepard",
|
||||
Role::MasterWolf => "Master Wolf",
|
||||
Role::WolfShaman => "Wolf Shaman",
|
||||
Role::Wolf => "Wolf",
|
||||
Role::Custom(role_name, _) => role_name,
|
||||
};
|
||||
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Role {
|
||||
/// Color as seen by host or seer
|
||||
pub fn seer_color(&self) -> RoleColor {
|
||||
match self {
|
||||
Role::Villager => RoleColor::Green,
|
||||
Role::Seer => RoleColor::Blue,
|
||||
Role::Guardian => RoleColor::Blue,
|
||||
Role::Jailer => RoleColor::Blue,
|
||||
Role::Coroner => RoleColor::Blue,
|
||||
Role::Vigilante => RoleColor::Blue,
|
||||
Role::Hunter => RoleColor::Blue,
|
||||
Role::Psychic => RoleColor::Blue,
|
||||
Role::Miller => RoleColor::Red,
|
||||
Role::Herring => RoleColor::Blue,
|
||||
Role::Spy => RoleColor::Blue,
|
||||
Role::Jester => RoleColor::Purple,
|
||||
Role::Mason => RoleColor::Green,
|
||||
Role::Shepard => RoleColor::Green,
|
||||
Role::MasterWolf => RoleColor::Green,
|
||||
Role::WolfShaman => RoleColor::Red,
|
||||
Role::Wolf => RoleColor::Red,
|
||||
Role::Custom(_, color) => color.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Color as seen by player
|
||||
pub fn player_color(&self) -> RoleColor {
|
||||
match self {
|
||||
Role::Miller => RoleColor::Green,
|
||||
_ => self.seer_color(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Role name as seen by the player
|
||||
pub fn player_role_name(&self) -> String {
|
||||
match self {
|
||||
Role::Miller => Role::Villager.to_string(),
|
||||
_ => self.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_role_listener(&self, listeners: &mut Listeners) {
|
||||
match self {
|
||||
Role::Spy => listeners.add_listener(Box::new(SpyListener {})),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
use crate::game::listener::{EventStatus, Listener, ListenerContext, Priority};
|
||||
use crate::game::message_router::{Median, MessageDest, MessageSource, send_private_message, WoxlfMessage};
|
||||
use crate::game::role::Role;
|
||||
use serenity::async_trait;
|
||||
use serenity::model::prelude::UserId;
|
||||
use serenity::utils::MessageBuilder;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SpyListener {}
|
||||
|
||||
#[async_trait]
|
||||
impl Listener for SpyListener {
|
||||
fn get_priority(&self) -> Priority {
|
||||
Priority::Logging
|
||||
}
|
||||
|
||||
async fn on_chat(
|
||||
&mut self,
|
||||
ctx: &mut ListenerContext,
|
||||
msg: &WoxlfMessage,
|
||||
) -> crate::error::Result<EventStatus> {
|
||||
if msg.median != Median::DirectMessage {
|
||||
return Ok(EventStatus::Okay);
|
||||
}
|
||||
|
||||
let src_player = if let MessageSource::Player(p) = &msg.source {
|
||||
p
|
||||
} else {
|
||||
return Ok(EventStatus::Okay);
|
||||
};
|
||||
|
||||
let dest_player = if let MessageDest::Player(p) = &msg.dest {
|
||||
p
|
||||
} else {
|
||||
return Ok(EventStatus::Okay);
|
||||
};
|
||||
|
||||
|
||||
let spy_player = ctx
|
||||
.data
|
||||
.game_state()?
|
||||
.player_data
|
||||
.iter()
|
||||
.find(|p| p.alive && p.role == Role::Spy);
|
||||
|
||||
if let Some(spy_player) = spy_player {
|
||||
if spy_player.discord_id == dest_player.discord_id || spy_player.discord_id == src_player.discord_id {
|
||||
return Ok(EventStatus::Okay);
|
||||
}
|
||||
|
||||
let msg_content = MessageBuilder::default()
|
||||
.push_bold_line_safe(format!(
|
||||
"{} Sent {} a private message:",
|
||||
src_player.codename, dest_player.codename
|
||||
))
|
||||
.push(msg.content.clone())
|
||||
.build();
|
||||
|
||||
send_private_message(&ctx.ctx.http, UserId::from(spy_player.discord_id), &msg_content, &None).await?
|
||||
}
|
||||
|
||||
return Ok(EventStatus::Okay);
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ use game::global_data::GlobalData;
|
|||
|
||||
use crate::config::{Args, BotConfig};
|
||||
use crate::game::listener::Listeners;
|
||||
use crate::game::role::Role;
|
||||
|
||||
mod config;
|
||||
mod discord;
|
||||
|
@ -32,11 +33,14 @@ async fn main() {
|
|||
.expect("Unable to open saved game state.");
|
||||
}
|
||||
|
||||
let mut listeners = Listeners::default();
|
||||
Role::Spy.register_role_listener(&mut listeners);
|
||||
|
||||
let mut client = Client::builder(&bot_cfg.discord_config.token, GatewayIntents::all())
|
||||
.event_handler(Handler {})
|
||||
.framework(command_framework())
|
||||
.type_map_insert::<GlobalData>(Arc::new(Mutex::new(global_data)))
|
||||
.type_map_insert::<Listeners>(Arc::new(Mutex::new(Listeners::default())))
|
||||
.type_map_insert::<Listeners>(Arc::new(Mutex::new(listeners)))
|
||||
.await
|
||||
.expect("Err creating client");
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::config::MessageConfig;
|
||||
use crate::game::player_data::PlayerData;
|
||||
use crate::game::role::Role;
|
||||
use crate::GlobalData;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -33,6 +34,34 @@ fn time_to_discord_time(time_flag: &str) -> TeraFnRet {
|
|||
)
|
||||
}
|
||||
|
||||
fn role_name() -> TeraFnRet {
|
||||
Box::new(
|
||||
move |args: &HashMap<String, Value>| -> tera::Result<Value> {
|
||||
match args.get("role") {
|
||||
Some(val) => match tera::from_value::<Role>(val.clone()) {
|
||||
Ok(v) => Ok(tera::to_value(v.player_role_name()).unwrap()),
|
||||
Err(_) => Err("Failed to parse value as role".into()),
|
||||
},
|
||||
None => Err("Missing parameter".into()),
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn role_color() -> TeraFnRet {
|
||||
Box::new(
|
||||
move |args: &HashMap<String, Value>| -> tera::Result<Value> {
|
||||
match args.get("role") {
|
||||
Some(val) => match tera::from_value::<Role>(val.clone()) {
|
||||
Ok(v) => Ok(tera::to_value(v.player_color().to_string()).unwrap()),
|
||||
Err(_) => Err("Failed to parse value as role".into()),
|
||||
},
|
||||
None => Err("Missing parameter".into()),
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DiscordUser {
|
||||
pub(crate) display_name: String,
|
||||
|
@ -157,6 +186,8 @@ impl TryFrom<MessageConfig> for MessageTemplates {
|
|||
|
||||
templates.register_function("to_countdown", time_to_discord_time("R"));
|
||||
templates.register_function("to_local_time", time_to_discord_time("f"));
|
||||
templates.register_function("player_color", role_color());
|
||||
templates.register_function("player_role", role_name());
|
||||
|
||||
templates.add_raw_template("welcome_message", &config.welcome_message)?;
|
||||
templates.add_raw_template("status_message", &config.status_message)?;
|
||||
|
|
Loading…
Reference in New Issue