Added config for themes. Fixes #1
* WOxlf now supports a number of game themes * Each theme can have custom messages, profile pics, phrasing, etc * Messages are defined as tera templates * Couple small improvements + Clippy + fmtmsg_refactor
parent
2e5c102887
commit
230930a23c
|
@ -124,6 +124,15 @@ dependencies = [
|
|||
"byte-tools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.9.1"
|
||||
|
@ -180,6 +189,28 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz-build",
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz-build"
|
||||
version = "0.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069"
|
||||
dependencies = [
|
||||
"parse-zoneinfo",
|
||||
"phf",
|
||||
"phf_codegen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.34.0"
|
||||
|
@ -259,6 +290,22 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deunicode"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.8.1"
|
||||
|
@ -483,6 +530,30 @@ dependencies = [
|
|||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"fnv",
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globwalk"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.11"
|
||||
|
@ -569,6 +640,12 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.17"
|
||||
|
@ -630,6 +707,24 @@ dependencies = [
|
|||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"globset",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex",
|
||||
"same-file",
|
||||
"thread_local",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.8.0"
|
||||
|
@ -917,6 +1012,15 @@ dependencies = [
|
|||
"hashbrown 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-zoneinfo"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.1"
|
||||
|
@ -972,6 +1076,45 @@ dependencies = [
|
|||
"sha-1 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
"uncased",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.0.10"
|
||||
|
@ -1283,6 +1426,15 @@ version = "1.0.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.19"
|
||||
|
@ -1431,12 +1583,27 @@ dependencies = [
|
|||
"opaque-debug 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
|
||||
|
||||
[[package]]
|
||||
name = "slug"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
|
||||
dependencies = [
|
||||
"deunicode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.4"
|
||||
|
@ -1514,6 +1681,28 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tera"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3cac831b615c25bcef632d1cabf864fa05813baad3d526829db18eb70e8b58d"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"globwalk",
|
||||
"humansize",
|
||||
"lazy_static",
|
||||
"percent-encoding",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slug",
|
||||
"unic-segment",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.11.0"
|
||||
|
@ -1523,6 +1712,15 @@ dependencies = [
|
|||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.44"
|
||||
|
@ -1714,6 +1912,65 @@ version = "0.1.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
|
||||
|
||||
[[package]]
|
||||
name = "uncased"
|
||||
version = "0.9.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-property"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
|
||||
dependencies = [
|
||||
"unic-char-range",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-range"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
|
||||
|
||||
[[package]]
|
||||
name = "unic-common"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
|
||||
|
||||
[[package]]
|
||||
name = "unic-segment"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
|
||||
dependencies = [
|
||||
"unic-ucd-segment",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-ucd-segment"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
|
||||
dependencies = [
|
||||
"unic-char-property",
|
||||
"unic-char-range",
|
||||
"unic-ucd-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-ucd-version"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
|
||||
dependencies = [
|
||||
"unic-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.6.0"
|
||||
|
@ -1804,6 +2061,17 @@ version = "0.9.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.0"
|
||||
|
@ -1956,6 +2224,15 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
|
@ -1984,6 +2261,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serenity",
|
||||
"structopt",
|
||||
"tera",
|
||||
"tokio",
|
||||
"toml",
|
||||
]
|
||||
|
|
|
@ -15,6 +15,7 @@ toml = "0.5.8"
|
|||
regex = "1.5.5"
|
||||
futures = "0.3.21"
|
||||
reqwest = "0.11.10"
|
||||
tera = "1.15.0"
|
||||
|
||||
[dependencies.serenity]
|
||||
version = "0.10.10"
|
||||
|
|
103
README.md
103
README.md
|
@ -34,25 +34,98 @@ The game proceeds as a normal Werewolf game.
|
|||
|
||||
## Example Config
|
||||
```toml
|
||||
# Discord Bot Token
|
||||
token = ""
|
||||
# Discord App Id
|
||||
app_id = 0
|
||||
# Channel to accept host commands from
|
||||
host_channel = 1
|
||||
# Channel to put vote status messages in
|
||||
vote_channel = 2
|
||||
# Category to crate the player cannels in
|
||||
category = 3
|
||||
# Directory to save game data into, game data will be saved as "wOxlf.toml"
|
||||
# Directory to store the game state in
|
||||
game_state_dir = "."
|
||||
# imgur API client id
|
||||
imgur_client_id = ""
|
||||
|
||||
# Random code names are generated in the format of <Adjective> <Occupation>
|
||||
# Discord Bot Config
|
||||
[discord_config]
|
||||
# Bot token
|
||||
token = ""
|
||||
# Bor app id
|
||||
app_id = 0
|
||||
# Channel for the host to interact the bot
|
||||
host_channel = 949483310613143564
|
||||
# Webhook id for the host channel
|
||||
host_webhook_id = 955135068341415996
|
||||
# CHannel to post vote status
|
||||
vote_channel = 950078550717923329
|
||||
# Category to create player channels under
|
||||
category = 949766593322303518
|
||||
|
||||
occupation = ["Engineer", "Scientist", "Dancer", "Farmer", "Captain", "Janitor", "Author", "Bartender", "Bum",
|
||||
"Student", "Teacher", "Chef", "Waiter", "Comedian"]
|
||||
# An example game config
|
||||
[[game_config]]
|
||||
# Game name, used to identify this config
|
||||
game_name = "rouge_ai"
|
||||
# The bot's name for this game
|
||||
bot_name = "WOxlf"
|
||||
# Vote phase name
|
||||
vote_phase_name = "Day"
|
||||
# Enemy phase name
|
||||
enemy_phase_name = "Night"
|
||||
# Imgur album hash for profile pics
|
||||
profile_album_hash = "Raf84L4"
|
||||
# PLayer group name
|
||||
player_group_name = "Test Subjects"
|
||||
|
||||
adjective = ["Sleepy", "Drunk", "Smart", "Gifted", "Extreme", "Eccentric", "Amazing", "Bad", "Silly", "Dumb", "Smelly"]
|
||||
# Names used for codename generation
|
||||
first_name = ["Sleepy", "Drunk", "Smart", "Gifted", "Extreme", "Eccentric", "Amazing", "Bad", "Silly", "Dumb", "Smelly",
|
||||
"Gooey", "Ok", "Poor", "Fast", "Gentle", "Dangerous", "Spooky", "Soft", "Small", "Big"]
|
||||
|
||||
last_name = ["Engineer", "Scientist", "Dancer", "Farmer", "Captain", "Janitor", "Author", "Bartender", "Bum",
|
||||
"Student", "Teacher", "Chef", "Waiter", "Comedian", "Doctor", "Athlete", "Gamer"]
|
||||
|
||||
# Message config for this game
|
||||
[game_config.messages]
|
||||
# Format of the codenames
|
||||
name_format = "{{ first_name }} {{ last_name }}"
|
||||
|
||||
# Welcome message to send to players at the start of a game
|
||||
welcome_message = '''
|
||||
Welcome {{ discord_user.mention }} to your WOxlf Terminal. You may use this terminal to communicate to other subjects.
|
||||
You will also use this terminal for choosing one of your fellow subjects for termination.
|
||||
|
||||
Happy testing :)
|
||||
|
||||
Do $help to see all commands.
|
||||
|
||||
**SUBJECT CODENAME: {{ player_data.codename }}**
|
||||
|
||||
'''
|
||||
|
||||
# Status message format
|
||||
status_message = '''
|
||||
**EXPERIMENT STATUS**
|
||||
CURRENT EXPERIMENT PHASE: {{ game_state.current_phase }} {{ game_state.phase_number}}
|
||||
PHASE END TIME: {{ to_local_time(time=game_state.phase_end_time) }}
|
||||
PHASE ENDING {{ to_countdown(time=game_state.phase_end_time) }}
|
||||
|
||||
**TEST SUBJECTS REMAINING:**
|
||||
{% for player in game_state.player_data %}* {{ player.codename }}
|
||||
{% endfor %}
|
||||
'''
|
||||
|
||||
# Wrapper for annoucments
|
||||
announcement_format = '''
|
||||
**\*\*IMPORTANT wOxlf SYSTEM MESSAGE\*\***
|
||||
{{ message }}
|
||||
**\*\*END OF SYSTEM MESSAGE\*\***
|
||||
'''
|
||||
|
||||
# Vote tally message format
|
||||
tally_message = '''
|
||||
{% if tallies | length == 0 %} NO TERMIANTION VOTES HAVE BEEN CAST! {% else %}
|
||||
**TERMINATION VOTE TALLIES:**
|
||||
{% for target, count in tallies %}* {{ target }}: {{ count }} {% endfor %}
|
||||
{% endif %}
|
||||
'''
|
||||
|
||||
# Vote message format, used in the vote status channel
|
||||
vote_message = "{{ player_data.codename }} has selected {{ target_data.codename }} for termination."
|
||||
|
||||
# Message to send whene extending a phase
|
||||
phase_extend_message = "THE EXPERIMENT PHASE HAS BEEN EXTENDED!!!!"
|
||||
```
|
||||
|
||||
## License
|
||||
|
|
|
@ -13,22 +13,44 @@ pub struct Args {
|
|||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct GameConfig {
|
||||
pub occupation: Vec<String>,
|
||||
pub adjective: Vec<String>,
|
||||
pub game_name: String,
|
||||
pub bot_name: String,
|
||||
pub vote_phase_name: String,
|
||||
pub enemy_phase_name: String,
|
||||
pub player_group_name: String,
|
||||
pub profile_album_hash: String,
|
||||
pub first_name: Vec<String>,
|
||||
pub last_name: Vec<String>,
|
||||
pub messages: MessageConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct BotConfig {
|
||||
pub struct MessageConfig {
|
||||
pub welcome_message: String,
|
||||
pub status_message: String,
|
||||
pub announcement_format: String,
|
||||
pub tally_message: String,
|
||||
pub vote_message: String,
|
||||
pub phase_extend_message: String,
|
||||
pub name_format: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct DiscordConfig {
|
||||
pub token: String,
|
||||
pub app_id: u64,
|
||||
pub host_channel: u64,
|
||||
pub host_webhook_id: u64,
|
||||
pub vote_channel: u64,
|
||||
pub category: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct BotConfig {
|
||||
pub imgur_client_id: String,
|
||||
pub game_state_dir: PathBuf,
|
||||
pub game_config: GameConfig,
|
||||
pub discord_config: DiscordConfig,
|
||||
pub game_config: Vec<GameConfig>,
|
||||
}
|
||||
|
||||
impl BotConfig {
|
||||
|
@ -40,6 +62,14 @@ impl BotConfig {
|
|||
cfg.try_deserialize()
|
||||
}
|
||||
|
||||
pub fn get_game_config(&self, game_name: &str) -> Option<GameConfig> {
|
||||
let game_name = game_name.to_lowercase();
|
||||
self.game_config
|
||||
.iter()
|
||||
.find(|g| g.game_name.to_lowercase() == game_name)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn get_game_state_path(&self) -> PathBuf {
|
||||
self.game_state_dir.join("wOxlf_data.toml")
|
||||
}
|
||||
|
|
|
@ -13,13 +13,12 @@ use serenity::model::prelude::{Message, UserId};
|
|||
use serenity::prelude::Context;
|
||||
use serenity::utils::MessageBuilder;
|
||||
|
||||
use crate::discord::helper::{
|
||||
add_user_to_game, build_system_message, parse_duration_arg, send_msg_to_player_channels,
|
||||
};
|
||||
use crate::discord::helper::{add_user_to_game, parse_duration_arg, send_msg_to_player_channels};
|
||||
use crate::error::{Result, WoxlfError};
|
||||
use crate::game::global_data::GlobalData;
|
||||
use crate::game::MessageSource;
|
||||
use crate::game::Phase;
|
||||
use crate::messages::DiscordUser;
|
||||
|
||||
#[group]
|
||||
#[commands(start, say, end, broadcast, next_phase, terminate, add_time)]
|
||||
|
@ -36,9 +35,10 @@ async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||
let mut global_data = global_data.lock().await;
|
||||
|
||||
let game_name = args.single::<String>()?;
|
||||
let duration = parse_duration_arg(&mut args).await?;
|
||||
|
||||
global_data.start_game(Phase::Night, duration.into());
|
||||
global_data.start_game(&game_name, Phase::Night, duration.into())?;
|
||||
|
||||
let players: Result<Vec<&Member>> = args
|
||||
.iter::<String>()
|
||||
|
@ -77,12 +77,38 @@ async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||
|
||||
players.shuffle(&mut thread_rng());
|
||||
|
||||
let mut first_names = global_data.game_cfg()?.first_name.clone();
|
||||
let mut last_names = global_data.game_cfg()?.last_name.clone();
|
||||
|
||||
first_names.shuffle(&mut thread_rng());
|
||||
last_names.shuffle(&mut thread_rng());
|
||||
|
||||
for player in players {
|
||||
add_user_to_game(ctx, &guild, &mut global_data, player).await?;
|
||||
let first_name = first_names.pop();
|
||||
let last_name = last_names.pop();
|
||||
add_user_to_game(ctx, &guild, &mut global_data, player, first_name, last_name).await?;
|
||||
}
|
||||
|
||||
for player_data in &global_data.game_state()?.player_data {
|
||||
let channel = ChannelId::from(player_data.channel);
|
||||
let discord_user = guild.member(&ctx.http, player_data.discord_id).await?;
|
||||
|
||||
let intro_message = global_data.templates()?.build_welcome_message(
|
||||
&global_data,
|
||||
&DiscordUser::from(&discord_user),
|
||||
player_data,
|
||||
)?;
|
||||
|
||||
let msg = channel
|
||||
.send_message(&ctx.http, |m| m.content(intro_message))
|
||||
.await?;
|
||||
channel.pin(&ctx.http, msg.id).await?;
|
||||
}
|
||||
|
||||
global_data.save_game_state().unwrap();
|
||||
|
||||
ctx.http.edit_nickname(guild.id.0, Some(&global_data.game_cfg()?.bot_name)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -107,6 +133,7 @@ async fn end(ctx: &Context, msg: &Message, mut _args: Args) -> CommandResult {
|
|||
|
||||
global_data.clear_game_state()?;
|
||||
|
||||
ctx.http.edit_nickname(guild.id.0, None).await?;
|
||||
msg.reply(&ctx.http, "Game ended!").await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
@ -121,14 +148,12 @@ async fn say(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|||
|
||||
let mut global_data = global_data.lock().await;
|
||||
|
||||
let msg = format!("**wOxlf **> {}", args.rest());
|
||||
|
||||
send_msg_to_player_channels(
|
||||
ctx,
|
||||
&guild,
|
||||
&mut global_data,
|
||||
MessageSource::Host,
|
||||
&msg,
|
||||
args.rest(),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
|
@ -147,7 +172,9 @@ async fn broadcast(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|||
|
||||
let mut global_data = global_data.lock().await;
|
||||
|
||||
let msg = build_system_message(args.rest());
|
||||
let msg = global_data
|
||||
.templates()?
|
||||
.build_announcement(&global_data, args.rest())?;
|
||||
|
||||
send_msg_to_player_channels(
|
||||
ctx,
|
||||
|
@ -180,10 +207,12 @@ async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
|||
let broadcast = MessageBuilder::new()
|
||||
.push_line(args.rest())
|
||||
.push_line("")
|
||||
.push(global_data.print_game_status())
|
||||
.push(global_data.templates()?.build_satus_message(&global_data)?)
|
||||
.build();
|
||||
|
||||
let broadcast = build_system_message(&broadcast);
|
||||
let broadcast = global_data
|
||||
.templates()?
|
||||
.build_announcement(&global_data, &broadcast)?;
|
||||
|
||||
send_msg_to_player_channels(
|
||||
ctx,
|
||||
|
@ -199,13 +228,16 @@ async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
|||
if global_data.game_state_mut()?.current_phase == Phase::Day {
|
||||
let vote_channel = guild
|
||||
.channels
|
||||
.get(&ChannelId::from(global_data.cfg.vote_channel))
|
||||
.get(&ChannelId::from(
|
||||
global_data.cfg.discord_config.vote_channel,
|
||||
))
|
||||
.unwrap();
|
||||
vote_channel
|
||||
.send_message(&ctx.http, |m| {
|
||||
m.content(format!(
|
||||
"**DAY {} VOTES:**",
|
||||
global_data.game_state_mut().unwrap().phase_number
|
||||
"**{} {} Votes:**",
|
||||
global_data.game_cfg().unwrap().vote_phase_name.clone(),
|
||||
&global_data.game_state_mut().unwrap().phase_number
|
||||
))
|
||||
})
|
||||
.await?;
|
||||
|
@ -215,7 +247,7 @@ async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
|||
&ctx.http,
|
||||
format!(
|
||||
"Phase has been cycled to {}.",
|
||||
&global_data.game_state_mut()?.current_phase
|
||||
&global_data.get_phase_name()?
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
@ -252,14 +284,11 @@ async fn terminate(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|||
|
||||
player_channel.delete(&ctx.http).await.unwrap();
|
||||
|
||||
msg.reply(
|
||||
&ctx.http,
|
||||
format!("{} has been terminated.", player.codename),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
msg.reply(&ctx.http, format!("{} ", player.codename))
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
msg.reply(&ctx.http, "No subject found with that codename.")
|
||||
msg.reply(&ctx.http, "No player found with that codename.")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
@ -285,12 +314,18 @@ async fn add_time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
.add_time_to_phase(duration.into());
|
||||
|
||||
let broadcast = MessageBuilder::new()
|
||||
.push_line("EXPERIMENT PHASE HAS BEEN EXTENDED!!!")
|
||||
.push(
|
||||
global_data
|
||||
.templates()?
|
||||
.build_phase_extend_message(&global_data)?,
|
||||
)
|
||||
.push_line("")
|
||||
.push(global_data.print_game_status())
|
||||
.push(global_data.templates()?.build_satus_message(&global_data)?)
|
||||
.build();
|
||||
|
||||
let broadcast = build_system_message(&broadcast);
|
||||
let broadcast = global_data
|
||||
.templates()?
|
||||
.build_announcement(&global_data, &broadcast)?;
|
||||
|
||||
send_msg_to_player_channels(
|
||||
ctx,
|
||||
|
@ -328,7 +363,10 @@ async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|||
if global_data.game_state_mut()?.current_phase != Phase::Day {
|
||||
msg.reply(
|
||||
&ctx.http,
|
||||
"You can only select subject for termination during the day!",
|
||||
format!(
|
||||
"You can only vote during the {} phase.",
|
||||
global_data.game_cfg()?.vote_phase_name
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -347,7 +385,9 @@ async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|||
if let Some(target_player) = target_player {
|
||||
let vote_channel = guild
|
||||
.channels
|
||||
.get(&ChannelId::from(global_data.cfg.vote_channel))
|
||||
.get(&ChannelId::from(
|
||||
global_data.cfg.discord_config.vote_channel,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let player_data = global_data
|
||||
|
@ -357,25 +397,30 @@ async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|||
|
||||
player_data.cast_vote(target_player.discord_id);
|
||||
|
||||
vote_channel
|
||||
.send_message(&ctx.http, |m| {
|
||||
m.content(format!(
|
||||
"{} has selected {} for termination",
|
||||
&player_data.codename, target_player.codename
|
||||
))
|
||||
})
|
||||
.await
|
||||
// borrow as immutable
|
||||
let player_data = global_data
|
||||
.game_state()?
|
||||
.get_player_from_channel(msg.channel_id.0)
|
||||
.unwrap();
|
||||
|
||||
let vote_msg = global_data.templates()?.build_vote_message(
|
||||
&global_data,
|
||||
player_data,
|
||||
&target_player,
|
||||
)?;
|
||||
|
||||
vote_channel
|
||||
.send_message(&ctx.http, |m| m.content(vote_msg))
|
||||
.await?;
|
||||
} else {
|
||||
msg.reply(&ctx.http, "Subject not found!").await.unwrap();
|
||||
msg.reply(&ctx.http, "Target not found!").await.unwrap();
|
||||
}
|
||||
} else {
|
||||
msg.reply(
|
||||
&ctx.http,
|
||||
"This command needs to be run in a game channel, goober",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
}
|
||||
|
||||
global_data.save_game_state().unwrap();
|
||||
|
@ -384,7 +429,7 @@ async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[description = "Get the game status. $status"]
|
||||
#[description = "Get the game status."]
|
||||
async fn status(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
||||
let mut data = ctx.data.write().await;
|
||||
let global_data = data.get_mut::<GlobalData>().unwrap();
|
||||
|
@ -393,21 +438,20 @@ async fn status(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
|||
|
||||
let mut msg_builder = MessageBuilder::new();
|
||||
|
||||
msg_builder.push(global_data.print_game_status());
|
||||
msg_builder.push(
|
||||
global_data
|
||||
.templates()?
|
||||
.build_satus_message(&global_data)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
if global_data.game_state_mut()?.current_phase == Phase::Day {
|
||||
msg_builder.push_line("");
|
||||
|
||||
let vote_tallies = global_data.game_state_mut()?.get_vote_tallies();
|
||||
|
||||
if vote_tallies.is_empty() {
|
||||
msg_builder.push_line("NO TERMINATION VOTES HAVE BEEN CAST");
|
||||
} else {
|
||||
msg_builder.push_line("TERMINATION VOTE TALLIES:");
|
||||
for (player, tally) in global_data.game_state_mut()?.get_vote_tallies() {
|
||||
msg_builder.push_line(format!("{}: {}", player, tally));
|
||||
}
|
||||
}
|
||||
msg_builder.push_line(
|
||||
global_data
|
||||
.templates()?
|
||||
.build_vote_tally(&global_data)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
msg.reply(&ctx.http, msg_builder.build()).await.unwrap();
|
||||
|
@ -417,7 +461,7 @@ async fn status(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
|||
|
||||
#[command]
|
||||
#[only_in(guilds)]
|
||||
#[description = "Get the other players in the game. $status"]
|
||||
#[description = "Get the other players in the game."]
|
||||
async fn players(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let global_data = data.get::<GlobalData>().unwrap();
|
||||
|
@ -426,12 +470,12 @@ async fn players(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
|||
|
||||
let mut msg_builder = MessageBuilder::new();
|
||||
|
||||
msg_builder.push_line("Test Subjects:");
|
||||
msg_builder.push_line(&global_data.game_cfg()?.player_group_name);
|
||||
|
||||
for player in &global_data.game_state()?.player_data {
|
||||
msg_builder.push("* ").push(&player.codename);
|
||||
|
||||
if msg.channel_id.0 == global_data.cfg.host_channel {
|
||||
if msg.channel_id.0 == global_data.cfg.discord_config.host_channel {
|
||||
let guild = msg.guild(&ctx.cache).await.unwrap();
|
||||
let member = guild.members.get(&UserId::from(player.discord_id)).unwrap();
|
||||
msg_builder.push_line(format!(" ({})", member.display_name()));
|
||||
|
|
|
@ -5,14 +5,13 @@ use serenity::model::channel::{Message, PermissionOverwrite, PermissionOverwrite
|
|||
use serenity::model::guild::{Guild, Member};
|
||||
use serenity::model::id::{ChannelId, UserId};
|
||||
use serenity::model::Permissions;
|
||||
use serenity::utils::MessageBuilder;
|
||||
|
||||
use crate::error;
|
||||
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::MessageSource;
|
||||
use crate::{error, game};
|
||||
use serenity::prelude::SerenityError;
|
||||
|
||||
fn filter_source_channel(player_data: &&PlayerData, msg_source: &MessageSource) -> bool {
|
||||
|
@ -75,7 +74,9 @@ pub async fn send_msg_to_player_channels(
|
|||
|
||||
let host_channel = guild
|
||||
.channels
|
||||
.get(&ChannelId::from(global_data.cfg.host_channel))
|
||||
.get(&ChannelId::from(
|
||||
global_data.cfg.discord_config.host_channel,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let source = match msg_source {
|
||||
|
@ -195,7 +196,7 @@ pub async fn send_webhook_msg_to_player_channels(
|
|||
|
||||
send_webhook_msg(
|
||||
&ctx.http,
|
||||
global_data.cfg.host_webhook_id,
|
||||
global_data.cfg.discord_config.host_webhook_id,
|
||||
&host_channel_username,
|
||||
profile_pic,
|
||||
msg,
|
||||
|
@ -210,16 +211,21 @@ pub async fn add_user_to_game(
|
|||
guild: &Guild,
|
||||
global_data: &mut GlobalData,
|
||||
discord_user: &Member,
|
||||
) -> error::Result<()> {
|
||||
let mut codename = game::generate_codename(&global_data.cfg);
|
||||
|
||||
while global_data.game_state_mut()?.codename_exists(&codename) {
|
||||
codename = game::generate_codename(&global_data.cfg);
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
) -> error::Result<PlayerData> {
|
||||
if first_name == None && last_name == None {
|
||||
return Err(WoxlfError::RanOutOfCodenames);
|
||||
}
|
||||
|
||||
let codename = global_data
|
||||
.templates()?
|
||||
.build_name(global_data, first_name, last_name)
|
||||
.unwrap();
|
||||
|
||||
let channel = guild
|
||||
.create_channel(&ctx.http, |c| {
|
||||
c.category(&ChannelId::from(global_data.cfg.category))
|
||||
c.category(&ChannelId::from(global_data.cfg.discord_config.category))
|
||||
.name(format!("{}'s Channel", discord_user.display_name()))
|
||||
})
|
||||
.await?;
|
||||
|
@ -242,24 +248,6 @@ pub async fn add_user_to_game(
|
|||
)
|
||||
.await?;
|
||||
|
||||
let msg = channel.send_message(&ctx.http, |m| {
|
||||
m.content(MessageBuilder::new()
|
||||
.push("Welcome ")
|
||||
.mention(discord_user)
|
||||
.push_line(" to your WOxlf Terminal. You may use this terminal to communicate to other subjects.")
|
||||
.push_line("You will also use this terminal for choosing one of your fellow subjects for termination.")
|
||||
.push_line("Happy testing :)")
|
||||
.push_line("")
|
||||
.push_line("Do $help to see all commands.")
|
||||
.push_line("")
|
||||
.push("SUBJECT CODENAME: ")
|
||||
.push_line(&codename)
|
||||
.push(global_data.print_game_status())
|
||||
)
|
||||
}).await?;
|
||||
|
||||
channel.pin(&ctx.http, msg.id).await?;
|
||||
|
||||
let player_data = PlayerData {
|
||||
channel: channel.id.0,
|
||||
discord_id: discord_user.user.id.0,
|
||||
|
@ -269,19 +257,12 @@ pub async fn add_user_to_game(
|
|||
profile_pic_url: global_data.get_profile_pic_url().await?,
|
||||
};
|
||||
|
||||
global_data.game_state_mut()?.player_data.push(player_data);
|
||||
global_data
|
||||
.game_state_mut()?
|
||||
.player_data
|
||||
.push(player_data.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build_system_message(msg: &str) -> String {
|
||||
MessageBuilder::new()
|
||||
.push_bold_line("\\*\\*IMPORTANT wOxlf SYSTEM MESSAGE\\*\\*")
|
||||
.push_line("")
|
||||
.push_line(msg)
|
||||
.push_line("")
|
||||
.push_bold_line("\\*\\*END OF SYSTEM MESSAGE\\*\\*")
|
||||
.build()
|
||||
Ok(player_data)
|
||||
}
|
||||
|
||||
pub async fn parse_duration_arg(args: &mut Args) -> error::Result<PhaseDuration> {
|
||||
|
|
14
src/error.rs
14
src/error.rs
|
@ -1,6 +1,7 @@
|
|||
use crate::imgur::ImgurError;
|
||||
use serenity::prelude::SerenityError;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use tera::Error;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, WoxlfError>;
|
||||
|
||||
|
@ -15,6 +16,8 @@ pub enum WoxlfError {
|
|||
GameNotInProgress,
|
||||
HostWebhookError,
|
||||
ImgurError(ImgurError),
|
||||
RanOutOfCodenames,
|
||||
TemplateError(tera::Error),
|
||||
}
|
||||
|
||||
impl std::error::Error for WoxlfError {}
|
||||
|
@ -31,6 +34,11 @@ impl Display for WoxlfError {
|
|||
WoxlfError::GameNotInProgress => "A game is not currently in progress".to_string(),
|
||||
WoxlfError::HostWebhookError => "Unable to communicate to the host webhook".to_string(),
|
||||
WoxlfError::ImgurError(err) => format!("Imgur module error: {}", err),
|
||||
WoxlfError::RanOutOfCodenames => {
|
||||
"Ran out of codename combinations, add more first/last names to the config"
|
||||
.to_string()
|
||||
}
|
||||
WoxlfError::TemplateError(e) => format!("Template error: {}", e),
|
||||
};
|
||||
|
||||
write!(f, "Woxlf Error: {}", msg)
|
||||
|
@ -66,3 +74,9 @@ impl From<ImgurError> for WoxlfError {
|
|||
Self::ImgurError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tera::Error> for WoxlfError {
|
||||
fn from(err: Error) -> Self {
|
||||
Self::TemplateError(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ use crate::game::Phase;
|
|||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct GameState {
|
||||
pub game_name: String,
|
||||
pub phase_number: u64,
|
||||
pub current_phase: Phase,
|
||||
pub starting_phase: Phase,
|
||||
|
@ -19,9 +20,10 @@ pub struct GameState {
|
|||
}
|
||||
|
||||
impl GameState {
|
||||
pub fn new(starting_phase: Phase, starting_phase_duration: Duration) -> Self {
|
||||
pub fn new(starting_phase: Phase, starting_phase_duration: Duration, game_name: &str) -> Self {
|
||||
let phase_end_time = Utc::now() + starting_phase_duration;
|
||||
Self {
|
||||
game_name: game_name.to_string(),
|
||||
phase_number: 1,
|
||||
current_phase: starting_phase,
|
||||
phase_end_time,
|
||||
|
@ -30,17 +32,6 @@ impl GameState {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn codename_exists(&self, codename: &str) -> bool {
|
||||
let codename = codename.to_lowercase();
|
||||
|
||||
self.player_data.iter().any(|data| {
|
||||
let player_codename = data.codename.to_lowercase();
|
||||
let adj_occupation: Vec<&str> = player_codename.split(' ').collect();
|
||||
|
||||
codename.contains(adj_occupation[1])
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_player_from_channel(&self, channel_id: u64) -> Option<&PlayerData> {
|
||||
self.player_data.iter().find(|p| p.channel == channel_id)
|
||||
}
|
||||
|
@ -84,14 +75,6 @@ impl GameState {
|
|||
self.phase_end_time = Utc::now() + duration;
|
||||
}
|
||||
|
||||
pub fn get_phase_end_time(&self) -> String {
|
||||
format!("<t:{}:f>", self.phase_end_time.timestamp())
|
||||
}
|
||||
|
||||
pub fn get_phase_countdown(&self) -> String {
|
||||
format!("<t:{}:R>", self.phase_end_time.timestamp())
|
||||
}
|
||||
|
||||
pub fn add_time_to_phase(&mut self, duration: Duration) {
|
||||
self.phase_end_time = self.phase_end_time + duration;
|
||||
}
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::prelude::{Mutex, TypeMapKey};
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use crate::config::BotConfig;
|
||||
use crate::config::{BotConfig, GameConfig};
|
||||
use crate::error::{Result, WoxlfError};
|
||||
use crate::game::game_state::GameState;
|
||||
use crate::game::Phase;
|
||||
use crate::imgur::{get_album_images, Image};
|
||||
use crate::messages::MessageTemplates;
|
||||
use chrono::Duration;
|
||||
use rand::prelude::SliceRandom;
|
||||
use serenity::utils::MessageBuilder;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GlobalData {
|
||||
pub cfg: BotConfig,
|
||||
pub game_cfg: Option<GameConfig>,
|
||||
pub templates: Option<MessageTemplates>,
|
||||
pub game_state: Option<GameState>,
|
||||
}
|
||||
|
||||
|
@ -24,12 +25,29 @@ impl GlobalData {
|
|||
pub fn new(cfg: BotConfig) -> Self {
|
||||
Self {
|
||||
cfg,
|
||||
game_cfg: None,
|
||||
templates: None,
|
||||
game_state: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_game(&mut self, starting_phase: Phase, starting_phase_duration: Duration) {
|
||||
self.game_state = Some(GameState::new(starting_phase, starting_phase_duration))
|
||||
pub fn start_game(
|
||||
&mut self,
|
||||
game_name: &str,
|
||||
starting_phase: Phase,
|
||||
starting_phase_duration: Duration,
|
||||
) -> Result<()> {
|
||||
let game_config = self.cfg.get_game_config(game_name).unwrap();
|
||||
|
||||
self.templates = Some(MessageTemplates::try_from(game_config.messages.clone())?);
|
||||
self.game_cfg = Some(game_config);
|
||||
self.game_state = Some(GameState::new(
|
||||
starting_phase,
|
||||
starting_phase_duration,
|
||||
game_name,
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_game_state(&mut self) -> Result<()> {
|
||||
|
@ -59,6 +77,14 @@ impl GlobalData {
|
|||
|
||||
self.game_state = Some(toml::from_str(&data)?);
|
||||
|
||||
let game_config = self
|
||||
.cfg
|
||||
.get_game_config(&self.game_state.as_ref().unwrap().game_name)
|
||||
.unwrap();
|
||||
|
||||
self.templates = Some(MessageTemplates::try_from(game_config.messages.clone()).unwrap());
|
||||
self.game_cfg = Some(game_config);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -76,8 +102,23 @@ impl GlobalData {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn templates(&self) -> Result<&MessageTemplates> {
|
||||
match &self.templates {
|
||||
None => Err(WoxlfError::GameNotInProgress),
|
||||
Some(templates) => Ok(templates),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn game_cfg(&self) -> Result<&GameConfig> {
|
||||
match &self.game_cfg {
|
||||
None => Err(WoxlfError::GameNotInProgress),
|
||||
Some(game_config) => Ok(game_config),
|
||||
}
|
||||
}
|
||||
pub fn clear_game_state(&mut self) -> Result<()> {
|
||||
self.game_state = None;
|
||||
self.game_cfg = None;
|
||||
self.templates = None;
|
||||
|
||||
if self.game_state_exists() {
|
||||
std::fs::remove_file(self.cfg.get_game_state_path())?;
|
||||
|
@ -86,33 +127,26 @@ impl GlobalData {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn print_game_status(&self) -> String {
|
||||
if let Some(game_state) = &self.game_state {
|
||||
MessageBuilder::new()
|
||||
.push_line(format!(
|
||||
"CURRENT EXPERIMENT PHASE: {} {}",
|
||||
game_state.current_phase, game_state.phase_number
|
||||
))
|
||||
.push_line(format!(
|
||||
"PHASE END TIME: {}",
|
||||
game_state.get_phase_end_time()
|
||||
))
|
||||
.push_line(format!("PHASE ENDING {}", game_state.get_phase_countdown()))
|
||||
.build()
|
||||
} else {
|
||||
"Game not in progress.".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_profile_pic_url(&self) -> Result<String> {
|
||||
let images: Vec<Image> = get_album_images(
|
||||
&self.cfg.imgur_client_id,
|
||||
&self.cfg.game_config.profile_album_hash,
|
||||
&self.game_cfg()?.profile_album_hash,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(images.choose(&mut rand::thread_rng()).unwrap().link.clone())
|
||||
}
|
||||
|
||||
pub fn get_phase_name(&self) -> Result<String> {
|
||||
let game_cfg = self.game_cfg()?;
|
||||
let state = self.game_state()?;
|
||||
|
||||
Ok(if state.current_phase == Phase::Day {
|
||||
game_cfg.vote_phase_name.clone()
|
||||
} else {
|
||||
game_cfg.enemy_phase_name.clone()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeMapKey for GlobalData {
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use rand::thread_rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::BotConfig;
|
||||
use crate::game::player_data::PlayerData;
|
||||
use rand::prelude::SliceRandom;
|
||||
|
||||
pub mod game_state;
|
||||
pub mod global_data;
|
||||
|
@ -23,32 +18,6 @@ impl Default for Phase {
|
|||
}
|
||||
}
|
||||
|
||||
impl Display for Phase {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let phase_name = match self {
|
||||
Self::Day => "Day",
|
||||
Self::Night => "Night",
|
||||
};
|
||||
|
||||
write!(f, "{}", phase_name)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_codename(config: &BotConfig) -> String {
|
||||
let occupation = &config
|
||||
.game_config
|
||||
.occupation
|
||||
.choose(&mut thread_rng())
|
||||
.unwrap();
|
||||
let adj = &config
|
||||
.game_config
|
||||
.adjective
|
||||
.choose(&mut thread_rng())
|
||||
.unwrap();
|
||||
|
||||
format!("{} {}", adj, occupation)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MessageSource {
|
||||
Player(Box<PlayerData>),
|
||||
|
|
|
@ -15,6 +15,7 @@ mod discord;
|
|||
mod error;
|
||||
mod game;
|
||||
mod imgur;
|
||||
mod messages;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
|
@ -31,7 +32,7 @@ async fn main() {
|
|||
.expect("Unable to open saved game state.");
|
||||
}
|
||||
|
||||
let mut client = Client::builder(&bot_cfg.token)
|
||||
let mut client = Client::builder(&bot_cfg.discord_config.token)
|
||||
.event_handler(Handler {})
|
||||
.framework(command_framework())
|
||||
.intents(GatewayIntents::all())
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
use crate::config::MessageConfig;
|
||||
use crate::game::player_data::PlayerData;
|
||||
use crate::GlobalData;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::model::guild::Member;
|
||||
use serenity::prelude::Mentionable;
|
||||
use std::collections::HashMap;
|
||||
use tera::{Tera, Value};
|
||||
|
||||
fn time_to_discord_time(
|
||||
time_flag: &str,
|
||||
) -> Box<dyn Fn(&HashMap<String, Value>) -> tera::Result<Value> + Send + Sync> {
|
||||
let time_flag = time_flag.to_string();
|
||||
|
||||
Box::new(
|
||||
move |args: &HashMap<String, Value>| -> tera::Result<Value> {
|
||||
match args.get("time") {
|
||||
Some(val) => match tera::from_value::<String>(val.clone()) {
|
||||
Ok(v) => {
|
||||
let time = v.parse::<DateTime<Utc>>().unwrap();
|
||||
|
||||
Ok(
|
||||
tera::to_value(format!("<t:{}:{}>", time.timestamp(), time_flag))
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
Err(_) => Err("Failed to parse value as time".into()),
|
||||
},
|
||||
None => Err("Missing parameter".into()),
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DiscordUser {
|
||||
display_name: String,
|
||||
mention: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Tally {
|
||||
target: String,
|
||||
votes: u64,
|
||||
}
|
||||
|
||||
impl From<&Member> for DiscordUser {
|
||||
fn from(m: &Member) -> Self {
|
||||
Self {
|
||||
display_name: m.display_name().to_string(),
|
||||
mention: m.mention().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MessageTemplates {
|
||||
templates: Tera,
|
||||
}
|
||||
|
||||
impl MessageTemplates {
|
||||
pub fn build_welcome_message(
|
||||
&self,
|
||||
global_data: &GlobalData,
|
||||
discord_user: &DiscordUser,
|
||||
player_data: &PlayerData,
|
||||
) -> Result<String, tera::Error> {
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("game_cfg", global_data.game_cfg().unwrap());
|
||||
context.insert("game_state", global_data.game_state().unwrap());
|
||||
context.insert("discord_user", discord_user);
|
||||
context.insert("player_data", player_data);
|
||||
let mut msg = self.templates.render("welcome_message", &context)?;
|
||||
msg.push_str(&self.templates.render("status_message", &context)?);
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
pub fn build_satus_message(&self, global_data: &GlobalData) -> Result<String, tera::Error> {
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("game_cfg", global_data.game_cfg().unwrap());
|
||||
context.insert("game_state", global_data.game_state().unwrap());
|
||||
self.templates.render("status_message", &context)
|
||||
}
|
||||
|
||||
pub fn build_announcement(
|
||||
&self,
|
||||
global_data: &GlobalData,
|
||||
message: &str,
|
||||
) -> Result<String, tera::Error> {
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("game_cfg", global_data.game_cfg().unwrap());
|
||||
context.insert("game_state", global_data.game_state().unwrap());
|
||||
context.insert("message", message);
|
||||
self.templates.render("announcement", &context)
|
||||
}
|
||||
|
||||
pub fn build_vote_tally(&self, global_data: &GlobalData) -> Result<String, tera::Error> {
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("game_cfg", global_data.game_cfg().unwrap());
|
||||
context.insert("game_state", global_data.game_state().unwrap());
|
||||
context.insert(
|
||||
"tallies",
|
||||
&global_data.game_state().unwrap().get_vote_tallies(),
|
||||
);
|
||||
|
||||
self.templates.render("tally_message", &context)
|
||||
}
|
||||
|
||||
pub fn build_vote_message(
|
||||
&self,
|
||||
global_data: &GlobalData,
|
||||
player: &PlayerData,
|
||||
target: &PlayerData,
|
||||
) -> Result<String, tera::Error> {
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("game_cfg", global_data.game_cfg().unwrap());
|
||||
context.insert("game_state", global_data.game_state().unwrap());
|
||||
context.insert("player_data", player);
|
||||
context.insert("target_data", target);
|
||||
|
||||
self.templates.render("vote_message", &context)
|
||||
}
|
||||
|
||||
pub fn build_phase_extend_message(
|
||||
&self,
|
||||
global_data: &GlobalData,
|
||||
) -> Result<String, tera::Error> {
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("game_cfg", global_data.game_cfg().unwrap());
|
||||
context.insert("game_state", global_data.game_state().unwrap());
|
||||
|
||||
self.templates.render("phase_extend_message", &context)
|
||||
}
|
||||
|
||||
pub fn build_name(
|
||||
&self,
|
||||
global_data: &GlobalData,
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
) -> Result<String, tera::Error> {
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("game_cfg", global_data.game_cfg().unwrap());
|
||||
context.insert("first_name", &first_name);
|
||||
context.insert("last_name", &last_name);
|
||||
|
||||
self.templates.render("name_format", &context)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<MessageConfig> for MessageTemplates {
|
||||
type Error = tera::Error;
|
||||
|
||||
fn try_from(config: MessageConfig) -> Result<Self, Self::Error> {
|
||||
let mut templates = Tera::default();
|
||||
|
||||
templates.register_function("to_countdown", time_to_discord_time("R"));
|
||||
templates.register_function("to_local_time", time_to_discord_time("f"));
|
||||
|
||||
templates.add_raw_template("welcome_message", &config.welcome_message)?;
|
||||
templates.add_raw_template("status_message", &config.status_message)?;
|
||||
templates.add_raw_template("announcement", &config.announcement_format)?;
|
||||
templates.add_raw_template("tally_message", &config.tally_message)?;
|
||||
templates.add_raw_template("vote_message", &config.vote_message)?;
|
||||
templates.add_raw_template("phase_extend_message", &config.phase_extend_message)?;
|
||||
|
||||
templates.add_raw_template("name_format", &config.name_format)?;
|
||||
|
||||
Ok(Self { templates })
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue