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 + fmt
main
Joey Hines 2023-01-08 16:23:50 -07:00
parent 7af3bab486
commit 275f5c9305
Signed by: joeyahines
GPG Key ID: 995E531F7A569DDB
13 changed files with 289 additions and 22 deletions

View File

@ -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,

View File

@ -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 {
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 data = ctx.data.read().await;
let global_data = data.get::<GlobalData>().unwrap();
let mut global_data = global_data.lock().await;
let target = args.single::<String>()?;
let pm = args.rest();

View File

@ -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

View File

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

View File

@ -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);

View File

@ -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]

View File

@ -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,

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {})),
_ => {}
}
}
}

View File

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

View File

@ -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");

View File

@ -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)?;