reworked game setup
This commit is contained in:
parent
48828cac8a
commit
d664ff281d
|
|
@ -7,7 +7,7 @@ use std::{
|
||||||
use convert_case::Casing;
|
use convert_case::Casing;
|
||||||
use proc_macro2::Span;
|
use proc_macro2::Span;
|
||||||
use quote::{ToTokens, quote};
|
use quote::{ToTokens, quote};
|
||||||
use syn::{braced, bracketed, parenthesized, parse::Parse, parse_macro_input};
|
use syn::{parse::Parse, parse_macro_input};
|
||||||
|
|
||||||
mod checks;
|
mod checks;
|
||||||
pub(crate) mod hashlist;
|
pub(crate) mod hashlist;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::role::RoleTitle;
|
use crate::{message::PublicIdentity, player::PlayerId, role::RoleTitle};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
|
||||||
pub enum GameError {
|
pub enum GameError {
|
||||||
#[error("too many roles. have {players} players, but {roles} roles (incl wolves)")]
|
#[error("too many roles. there's {players} players, but {roles} roles")]
|
||||||
TooManyRoles { players: u8, roles: u8 },
|
TooManyRoles { players: u8, roles: u8 },
|
||||||
#[error("wolves range must start at 1")]
|
#[error("no wolves?")]
|
||||||
NoWolves,
|
NoWolves,
|
||||||
#[error("message invalid for game state")]
|
#[error("message invalid for game state")]
|
||||||
InvalidMessageForGameState,
|
InvalidMessageForGameState,
|
||||||
|
|
@ -29,8 +29,8 @@ pub enum GameError {
|
||||||
TimedOut,
|
TimedOut,
|
||||||
#[error("host channel closed")]
|
#[error("host channel closed")]
|
||||||
HostChannelClosed,
|
HostChannelClosed,
|
||||||
#[error("too few players: got {got} but the settings require at least {need}")]
|
#[error("too many players: there's {got} players but only {need} roles")]
|
||||||
TooFewPlayers { got: u8, need: u8 },
|
TooManyPlayers { got: u8, need: u8 },
|
||||||
#[error("it's already daytime")]
|
#[error("it's already daytime")]
|
||||||
AlreadyDaytime,
|
AlreadyDaytime,
|
||||||
#[error("it's not the end of the night yet")]
|
#[error("it's not the end of the night yet")]
|
||||||
|
|
@ -41,10 +41,10 @@ pub enum GameError {
|
||||||
NotNight,
|
NotNight,
|
||||||
#[error("invalid role, expected {expected:?} got {got:?}")]
|
#[error("invalid role, expected {expected:?} got {got:?}")]
|
||||||
InvalidRole { expected: RoleTitle, got: RoleTitle },
|
InvalidRole { expected: RoleTitle, got: RoleTitle },
|
||||||
#[error("villagers cannot be added to settings")]
|
|
||||||
CantAddVillagerToSettings,
|
|
||||||
#[error("no mentor for an apprentice to be an apprentice to :(")]
|
#[error("no mentor for an apprentice to be an apprentice to :(")]
|
||||||
NoApprenticeMentor,
|
NoApprenticeMentor,
|
||||||
|
#[error("{0} isn't a mentor role")]
|
||||||
|
NotAMentor(RoleTitle),
|
||||||
#[error("inactive game object")]
|
#[error("inactive game object")]
|
||||||
InactiveGameObject,
|
InactiveGameObject,
|
||||||
#[error("socket error: {0}")]
|
#[error("socket error: {0}")]
|
||||||
|
|
@ -71,4 +71,8 @@ pub enum GameError {
|
||||||
GuardianInvalidOriginalKill,
|
GuardianInvalidOriginalKill,
|
||||||
#[error("player not assigned number: {0}")]
|
#[error("player not assigned number: {0}")]
|
||||||
PlayerNotAssignedNumber(String),
|
PlayerNotAssignedNumber(String),
|
||||||
|
#[error("player [{0}] has an assigned role slot, but isn't in the player list")]
|
||||||
|
AssignedPlayerMissing(PlayerId),
|
||||||
|
#[error(" {0} assigned to {1} roles")]
|
||||||
|
AssignedMultipleTimes(PublicIdentity, usize),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,10 @@ use crate::{
|
||||||
},
|
},
|
||||||
player::CharacterId,
|
player::CharacterId,
|
||||||
};
|
};
|
||||||
pub use {settings::GameSettings, village::Village};
|
pub use {
|
||||||
|
settings::{Category, GameSettings, OrRandom, SetupRole, SetupSlot, SlotId},
|
||||||
|
village::Village,
|
||||||
|
};
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, GameError>;
|
type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -361,30 +361,17 @@ impl Night {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn received_response(&mut self, resp: ActionResponse) -> Result<ServerAction> {
|
pub fn received_response(&mut self, resp: ActionResponse) -> Result<ServerAction> {
|
||||||
// if let NightState::Active {
|
|
||||||
// current_prompt: ActionPrompt::CoverOfDarkness,
|
|
||||||
// current_result: None,
|
|
||||||
// } = &mut self.night_state
|
|
||||||
// && let ActionResponse::ClearCoverOfDarkness = &resp
|
|
||||||
// {
|
|
||||||
// self.night_state = NightState::Active {
|
|
||||||
// current_prompt: ActionPrompt::CoverOfDarkness,
|
|
||||||
// current_result: Some(ActionResult::Continue),
|
|
||||||
// };
|
|
||||||
// return Ok(ActionResult::Continue);
|
|
||||||
// }
|
|
||||||
|
|
||||||
match self.received_response_with_role_blocks(resp)? {
|
match self.received_response_with_role_blocks(resp)? {
|
||||||
BlockResolvedOutcome::PromptUpdate(prompt) => match &mut self.night_state {
|
BlockResolvedOutcome::PromptUpdate(prompt) => match &mut self.night_state {
|
||||||
NightState::Active {
|
NightState::Active {
|
||||||
current_result: Some(_),
|
current_result: Some(_),
|
||||||
..
|
..
|
||||||
} => return Err(GameError::AwaitingResponse),
|
} => Err(GameError::AwaitingResponse),
|
||||||
NightState::Active { current_prompt, .. } => {
|
NightState::Active { current_prompt, .. } => {
|
||||||
*current_prompt = prompt.clone();
|
*current_prompt = prompt.clone();
|
||||||
Ok(ServerAction::Prompt(prompt))
|
Ok(ServerAction::Prompt(prompt))
|
||||||
}
|
}
|
||||||
NightState::Complete => return Err(GameError::NightOver),
|
NightState::Complete => Err(GameError::NightOver),
|
||||||
},
|
},
|
||||||
BlockResolvedOutcome::ActionComplete(result, Some(change)) => {
|
BlockResolvedOutcome::ActionComplete(result, Some(change)) => {
|
||||||
match &mut self.night_state {
|
match &mut self.night_state {
|
||||||
|
|
|
||||||
|
|
@ -1,92 +1,205 @@
|
||||||
use super::Result;
|
mod settings_role;
|
||||||
use core::num::NonZeroU8;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
pub use settings_role::*;
|
||||||
|
|
||||||
|
use super::Result;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{error::GameError, role::RoleTitle};
|
use crate::{error::GameError, message::Identification, player::Character, role::RoleTitle};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct GameSettings {
|
pub struct GameSettings {
|
||||||
roles: HashMap<RoleTitle, NonZeroU8>,
|
roles: Vec<SetupSlot>,
|
||||||
|
next_order: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GameSettings {
|
impl Default for GameSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
roles: [
|
roles: vec![SetupSlot::new(RoleTitle::Werewolf, 0)],
|
||||||
(RoleTitle::Werewolf, NonZeroU8::new(1).unwrap()),
|
next_order: 1,
|
||||||
// (RoleTitle::Seer, NonZeroU8::new(1).unwrap()),
|
|
||||||
// (RoleTitle::Militia, NonZeroU8::new(1).unwrap()),
|
|
||||||
// (RoleTitle::Guardian, NonZeroU8::new(1).unwrap()),
|
|
||||||
// (RoleTitle::Apprentice, NonZeroU8::new(1).unwrap()),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameSettings {
|
impl GameSettings {
|
||||||
pub fn spread(&self) -> Box<[RoleTitle]> {
|
|
||||||
self.roles
|
|
||||||
.iter()
|
|
||||||
.flat_map(|(r, c)| [*r].repeat(c.get() as _))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
pub fn wolves_count(&self) -> usize {
|
pub fn wolves_count(&self) -> usize {
|
||||||
self.roles
|
self.roles
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(r, c)| {
|
.filter(|s| s.role.category().is_wolves())
|
||||||
if r.wolf() {
|
.count()
|
||||||
Some(c.get() as usize)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.sum()
|
pub fn slots(&self) -> &[SetupSlot] {
|
||||||
|
&self.roles
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn village_roles_count(&self) -> usize {
|
pub fn village_roles_count(&self) -> usize {
|
||||||
self.roles
|
log::warn!(
|
||||||
.iter()
|
"wolves: {} total: {}",
|
||||||
.filter_map(|(r, c)| {
|
self.wolves_count(),
|
||||||
if !r.wolf() {
|
self.roles.len()
|
||||||
Some(c.get() as usize)
|
);
|
||||||
} else {
|
self.roles.len() - self.wolves_count()
|
||||||
None
|
}
|
||||||
|
|
||||||
|
pub fn remove_assignments_not_in_list(&mut self, list: &[Identification]) {
|
||||||
|
self.roles.iter_mut().for_each(|r| {
|
||||||
|
if let Some(pid) = &r.assign_to
|
||||||
|
&& !list.iter().any(|i| i.player_id == *pid)
|
||||||
|
{
|
||||||
|
r.assign_to.take();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sum()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn roles(&self) -> Box<[(RoleTitle, NonZeroU8)]> {
|
pub fn remove_duplicate_assignments(&mut self) {
|
||||||
self.roles.iter().map(|(r, c)| (*r, *c)).collect()
|
let assignments = self
|
||||||
|
.roles
|
||||||
|
.iter()
|
||||||
|
.filter_map(|r| r.assign_to.as_ref())
|
||||||
|
.cloned()
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
let mut assignment_counter = HashMap::new();
|
||||||
|
for assign in assignments {
|
||||||
|
if let Some(counter) = assignment_counter.get_mut(&assign) {
|
||||||
|
*counter += 1;
|
||||||
|
} else {
|
||||||
|
assignment_counter.insert(assign, 1usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let to_remove = assignment_counter
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(pid, cnt)| (cnt > 1).then_some(pid))
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
|
||||||
|
for role in self.roles.iter_mut() {
|
||||||
|
if let Some(pid) = role.assign_to.as_ref()
|
||||||
|
&& to_remove.contains(pid)
|
||||||
|
{
|
||||||
|
role.assign_to.take();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn villagers_needed_for_player_count(&self, players: usize) -> Result<usize> {
|
pub fn assign(&self, players: &[Identification]) -> Result<Box<[Character]>> {
|
||||||
let min = self.min_players_needed();
|
self.check_with_player_list(players)?;
|
||||||
if min > players {
|
|
||||||
return Err(GameError::TooFewPlayers {
|
let roles_in_game = self
|
||||||
got: players as _,
|
.roles
|
||||||
need: min as _,
|
.iter()
|
||||||
|
.map(|r| r.role.clone().into())
|
||||||
|
.collect::<Box<[RoleTitle]>>();
|
||||||
|
|
||||||
|
let with_assigned_roles = self
|
||||||
|
.roles
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| {
|
||||||
|
s.assign_to.as_ref().map(|assign_to| {
|
||||||
|
players
|
||||||
|
.iter()
|
||||||
|
.find(|pid| pid.player_id == *assign_to)
|
||||||
|
.ok_or(GameError::AssignedPlayerMissing(assign_to.clone()))
|
||||||
|
.map(|id| (id, s))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Box<[_]>>>()?;
|
||||||
|
|
||||||
|
let mut random_assign_players = players
|
||||||
|
.iter()
|
||||||
|
.filter(|p| {
|
||||||
|
!with_assigned_roles
|
||||||
|
.iter()
|
||||||
|
.any(|(r, _)| r.player_id == p.player_id)
|
||||||
|
})
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
|
||||||
|
random_assign_players.shuffle(&mut rand::rng());
|
||||||
|
|
||||||
|
with_assigned_roles
|
||||||
|
.into_iter()
|
||||||
|
.chain(
|
||||||
|
random_assign_players
|
||||||
|
.into_iter()
|
||||||
|
.zip(self.roles.iter().filter(|s| s.assign_to.is_none())),
|
||||||
|
)
|
||||||
|
.map(|(id, slot)| slot.clone().into_character(id.clone(), &roles_in_game))
|
||||||
|
.collect::<Result<Box<[_]>>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_with_player_list(&self, players: &[Identification]) -> Result<()> {
|
||||||
|
self.check()?;
|
||||||
|
let (p_len, r_len) = (players.len(), self.roles.len());
|
||||||
|
if p_len > r_len {
|
||||||
|
return Err(GameError::TooManyPlayers {
|
||||||
|
got: p_len.min(0xFF) as u8,
|
||||||
|
need: self.roles.len().min(0xFF) as u8,
|
||||||
|
});
|
||||||
|
} else if p_len < r_len {
|
||||||
|
return Err(GameError::TooManyRoles {
|
||||||
|
players: p_len.min(0xFF) as u8,
|
||||||
|
roles: r_len.min(0xFF) as u8,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(players - self.roles.values().map(|c| c.get() as usize).sum::<usize>())
|
|
||||||
|
for role in self.roles.iter() {
|
||||||
|
if let Some(assigned) = role.assign_to.as_ref()
|
||||||
|
&& !players.iter().any(|p| p.player_id == *assigned)
|
||||||
|
{
|
||||||
|
return Err(GameError::AssignedPlayerMissing(assigned.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let assignments = self
|
||||||
|
.roles
|
||||||
|
.iter()
|
||||||
|
.filter_map(|r| r.assign_to.as_ref())
|
||||||
|
.cloned()
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
let mut assignment_counter = HashMap::new();
|
||||||
|
for assign in assignments {
|
||||||
|
if let Some(counter) = assignment_counter.get_mut(&assign) {
|
||||||
|
*counter += 1;
|
||||||
|
} else {
|
||||||
|
assignment_counter.insert(assign, 1usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((assign, cnt)) = assignment_counter.into_iter().find(|(_, cnt)| *cnt > 1) {
|
||||||
|
let ident = players
|
||||||
|
.iter()
|
||||||
|
.find(|i| i.player_id == assign)
|
||||||
|
.ok_or(GameError::AssignedPlayerMissing(assign))?;
|
||||||
|
return Err(GameError::AssignedMultipleTimes(ident.public.clone(), cnt));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check(&self) -> Result<()> {
|
pub fn check(&self) -> Result<()> {
|
||||||
if self.wolves_count() == 0 {
|
if self.wolves_count() == 0 {
|
||||||
return Err(GameError::NoWolves);
|
return Err(GameError::NoWolves);
|
||||||
}
|
}
|
||||||
if self
|
let mentor_count = self
|
||||||
.roles
|
.roles
|
||||||
.iter()
|
.iter()
|
||||||
.any(|(r, _)| matches!(r, RoleTitle::Apprentice))
|
.filter(|r| Into::<RoleTitle>::into(r.role.clone()).is_mentor())
|
||||||
&& self.roles.iter().filter(|(r, _)| r.is_mentor()).count() == 0
|
.count();
|
||||||
{
|
self.roles.iter().try_for_each(|s| match &s.role {
|
||||||
return Err(GameError::NoApprenticeMentor);
|
SetupRole::Apprentice { specifically: None } => (mentor_count > 0)
|
||||||
}
|
.then_some(())
|
||||||
|
.ok_or(GameError::NoApprenticeMentor),
|
||||||
|
SetupRole::Apprentice {
|
||||||
|
specifically: Some(role),
|
||||||
|
} => role
|
||||||
|
.is_mentor()
|
||||||
|
.then_some(())
|
||||||
|
.ok_or(GameError::NotAMentor(*role)),
|
||||||
|
_ => Ok(()),
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,26 +215,35 @@ impl GameSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(&mut self, role: RoleTitle) -> Result<()> {
|
pub fn new_slot(&mut self, role: RoleTitle) -> SlotId {
|
||||||
if role == RoleTitle::Villager {
|
let slot = SetupSlot::new(role, self.next_order);
|
||||||
return Err(GameError::CantAddVillagerToSettings);
|
self.next_order += 1;
|
||||||
}
|
let slot_id = slot.slot_id;
|
||||||
match self.roles.get_mut(&role) {
|
self.roles.push(slot);
|
||||||
Some(count) => *count = NonZeroU8::new(count.get() + 1).unwrap(),
|
self.sort_roles();
|
||||||
None => {
|
slot_id
|
||||||
self.roles.insert(role, NonZeroU8::new(1).unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sub(&mut self, role: RoleTitle) {
|
pub fn update_slot(&mut self, slot: SetupSlot) {
|
||||||
if let Some(count) = self.roles.get_mut(&role)
|
if let Some(old_slot) = self.roles.iter_mut().find(|r| r.slot_id == slot.slot_id) {
|
||||||
&& count.get() != 1
|
*old_slot = slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_slot(&mut self, slot_id: SlotId) {
|
||||||
|
if let Some(idx) = self
|
||||||
|
.roles
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find_map(|(idx, slot)| (slot.slot_id == slot_id).then_some(idx))
|
||||||
{
|
{
|
||||||
*count = NonZeroU8::new(count.get() - 1).unwrap();
|
self.roles.swap_remove(idx);
|
||||||
} else {
|
self.sort_roles();
|
||||||
self.roles.remove(&role);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sort_roles(&mut self) {
|
||||||
|
self.roles
|
||||||
|
.sort_by(|l, r| l.created_order.cmp(&r.created_order).reverse());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
use core::{
|
||||||
|
fmt::{Debug, Display},
|
||||||
|
num::NonZeroU8,
|
||||||
|
};
|
||||||
|
|
||||||
|
use rand::distr::{Distribution, StandardUniform};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use werewolves_macros::ChecksAs;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::GameError,
|
||||||
|
message::Identification,
|
||||||
|
modifier::Modifier,
|
||||||
|
player::{Character, PlayerId},
|
||||||
|
role::{Role, RoleTitle},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||||
|
pub enum OrRandom<T>
|
||||||
|
where
|
||||||
|
T: Debug + Clone + PartialEq,
|
||||||
|
StandardUniform: Distribution<T>,
|
||||||
|
{
|
||||||
|
Determined(T),
|
||||||
|
#[default]
|
||||||
|
Random,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> OrRandom<T>
|
||||||
|
where
|
||||||
|
for<'a> T: Debug + Clone + PartialEq,
|
||||||
|
StandardUniform: Distribution<T>,
|
||||||
|
{
|
||||||
|
pub fn into_concrete(self) -> T {
|
||||||
|
match self {
|
||||||
|
Self::Determined(value) => value,
|
||||||
|
Self::Random => rand::random(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, ChecksAs)]
|
||||||
|
pub enum Category {
|
||||||
|
#[checks]
|
||||||
|
Wolves,
|
||||||
|
Villager,
|
||||||
|
Intel,
|
||||||
|
Defensive,
|
||||||
|
Offensive,
|
||||||
|
StartsAsVillager,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ChecksAs)]
|
||||||
|
pub enum SetupRole {
|
||||||
|
#[checks(Category::Villager)]
|
||||||
|
Villager,
|
||||||
|
#[checks(Category::Villager)]
|
||||||
|
Scapegoat { redeemed: OrRandom<bool> },
|
||||||
|
#[checks(Category::Intel)]
|
||||||
|
Seer,
|
||||||
|
#[checks(Category::Intel)]
|
||||||
|
Arcanist,
|
||||||
|
#[checks(Category::Intel)]
|
||||||
|
Gravedigger,
|
||||||
|
#[checks(Category::Offensive)]
|
||||||
|
Hunter,
|
||||||
|
#[checks(Category::Offensive)]
|
||||||
|
Militia,
|
||||||
|
#[checks(Category::Offensive)]
|
||||||
|
MapleWolf,
|
||||||
|
#[checks(Category::Defensive)]
|
||||||
|
Guardian,
|
||||||
|
#[checks(Category::Defensive)]
|
||||||
|
Protector,
|
||||||
|
#[checks(Category::StartsAsVillager)]
|
||||||
|
Apprentice { specifically: Option<RoleTitle> },
|
||||||
|
#[checks(Category::StartsAsVillager)]
|
||||||
|
Elder { knows_on_night: NonZeroU8 },
|
||||||
|
|
||||||
|
#[checks(Category::Wolves)]
|
||||||
|
Werewolf,
|
||||||
|
#[checks(Category::Wolves)]
|
||||||
|
AlphaWolf,
|
||||||
|
#[checks(Category::Wolves)]
|
||||||
|
DireWolf,
|
||||||
|
#[checks(Category::Wolves)]
|
||||||
|
Shapeshifter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SetupRole {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
SetupRole::Villager => "Villager",
|
||||||
|
SetupRole::Scapegoat { .. } => "Scapegoat",
|
||||||
|
SetupRole::Seer => "Seer",
|
||||||
|
SetupRole::Arcanist => "Arcanist",
|
||||||
|
SetupRole::Gravedigger => "Gravedigger",
|
||||||
|
SetupRole::Hunter => "Hunter",
|
||||||
|
SetupRole::Militia => "Militia",
|
||||||
|
SetupRole::MapleWolf => "MapleWolf",
|
||||||
|
SetupRole::Guardian => "Guardian",
|
||||||
|
SetupRole::Protector => "Protector",
|
||||||
|
SetupRole::Apprentice { .. } => "Apprentice",
|
||||||
|
SetupRole::Elder { .. } => "Elder",
|
||||||
|
SetupRole::Werewolf => "Werewolf",
|
||||||
|
SetupRole::AlphaWolf => "AlphaWolf",
|
||||||
|
SetupRole::DireWolf => "DireWolf",
|
||||||
|
SetupRole::Shapeshifter => "Shapeshifter",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SetupRole {
|
||||||
|
pub fn into_role(self, roles_in_game: &[RoleTitle]) -> Result<Role, GameError> {
|
||||||
|
Ok(match self {
|
||||||
|
SetupRole::Villager => Role::Villager,
|
||||||
|
SetupRole::Scapegoat { redeemed } => Role::Scapegoat {
|
||||||
|
redeemed: redeemed.into_concrete(),
|
||||||
|
},
|
||||||
|
SetupRole::Seer => Role::Seer,
|
||||||
|
SetupRole::Arcanist => Role::Arcanist,
|
||||||
|
SetupRole::Gravedigger => Role::Gravedigger,
|
||||||
|
SetupRole::Hunter => Role::Hunter { target: None },
|
||||||
|
SetupRole::Militia => Role::Militia { targeted: None },
|
||||||
|
SetupRole::MapleWolf => Role::MapleWolf {
|
||||||
|
last_kill_on_night: 0,
|
||||||
|
},
|
||||||
|
SetupRole::Guardian => Role::Guardian {
|
||||||
|
last_protected: None,
|
||||||
|
},
|
||||||
|
SetupRole::Protector => Role::Protector {
|
||||||
|
last_protected: None,
|
||||||
|
},
|
||||||
|
SetupRole::Apprentice {
|
||||||
|
specifically: Some(role),
|
||||||
|
} => Role::Apprentice(role),
|
||||||
|
SetupRole::Apprentice { specifically: None } => {
|
||||||
|
let mentors = roles_in_game
|
||||||
|
.iter()
|
||||||
|
.filter(|r| r.is_mentor())
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
if mentors.is_empty() {
|
||||||
|
return Err(GameError::NoApprenticeMentor);
|
||||||
|
}
|
||||||
|
let mentor = *mentors[rand::random_range(0..mentors.len())];
|
||||||
|
Role::Apprentice(mentor)
|
||||||
|
}
|
||||||
|
SetupRole::Elder { knows_on_night } => Role::Elder {
|
||||||
|
knows_on_night,
|
||||||
|
has_protection: true,
|
||||||
|
},
|
||||||
|
SetupRole::Werewolf => Role::Werewolf,
|
||||||
|
SetupRole::AlphaWolf => Role::AlphaWolf { killed: None },
|
||||||
|
SetupRole::DireWolf => Role::DireWolf,
|
||||||
|
SetupRole::Shapeshifter => Role::Shapeshifter { shifted_into: None },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SetupRole> for RoleTitle {
|
||||||
|
fn from(value: SetupRole) -> Self {
|
||||||
|
match value {
|
||||||
|
SetupRole::Villager => RoleTitle::Villager,
|
||||||
|
SetupRole::Scapegoat { .. } => RoleTitle::Scapegoat,
|
||||||
|
SetupRole::Seer => RoleTitle::Seer,
|
||||||
|
SetupRole::Arcanist => RoleTitle::Arcanist,
|
||||||
|
SetupRole::Gravedigger => RoleTitle::Gravedigger,
|
||||||
|
SetupRole::Hunter => RoleTitle::Hunter,
|
||||||
|
SetupRole::Militia => RoleTitle::Militia,
|
||||||
|
SetupRole::MapleWolf => RoleTitle::MapleWolf,
|
||||||
|
SetupRole::Guardian => RoleTitle::Guardian,
|
||||||
|
SetupRole::Protector => RoleTitle::Protector,
|
||||||
|
SetupRole::Apprentice { .. } => RoleTitle::Apprentice,
|
||||||
|
SetupRole::Elder { .. } => RoleTitle::Elder,
|
||||||
|
SetupRole::Werewolf => RoleTitle::Werewolf,
|
||||||
|
SetupRole::AlphaWolf => RoleTitle::AlphaWolf,
|
||||||
|
SetupRole::DireWolf => RoleTitle::DireWolf,
|
||||||
|
SetupRole::Shapeshifter => RoleTitle::Shapeshifter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RoleTitle> for SetupRole {
|
||||||
|
fn from(value: RoleTitle) -> Self {
|
||||||
|
match value {
|
||||||
|
RoleTitle::Villager => SetupRole::Villager,
|
||||||
|
RoleTitle::Scapegoat => SetupRole::Scapegoat {
|
||||||
|
redeemed: Default::default(),
|
||||||
|
},
|
||||||
|
RoleTitle::Seer => SetupRole::Seer,
|
||||||
|
RoleTitle::Arcanist => SetupRole::Arcanist,
|
||||||
|
RoleTitle::Gravedigger => SetupRole::Gravedigger,
|
||||||
|
RoleTitle::Hunter => SetupRole::Hunter,
|
||||||
|
RoleTitle::Militia => SetupRole::Militia,
|
||||||
|
RoleTitle::MapleWolf => SetupRole::MapleWolf,
|
||||||
|
RoleTitle::Guardian => SetupRole::Guardian,
|
||||||
|
RoleTitle::Protector => SetupRole::Protector,
|
||||||
|
RoleTitle::Apprentice => SetupRole::Apprentice { specifically: None },
|
||||||
|
RoleTitle::Elder => SetupRole::Elder {
|
||||||
|
knows_on_night: NonZeroU8::new(3).unwrap(),
|
||||||
|
},
|
||||||
|
RoleTitle::Werewolf => SetupRole::Werewolf,
|
||||||
|
RoleTitle::AlphaWolf => SetupRole::AlphaWolf,
|
||||||
|
RoleTitle::DireWolf => SetupRole::DireWolf,
|
||||||
|
RoleTitle::Shapeshifter => SetupRole::Shapeshifter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct SlotId(Uuid);
|
||||||
|
|
||||||
|
impl SlotId {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(Uuid::new_v4())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct SetupSlot {
|
||||||
|
pub slot_id: SlotId,
|
||||||
|
pub role: SetupRole,
|
||||||
|
pub modifiers: Vec<Modifier>,
|
||||||
|
pub assign_to: Option<PlayerId>,
|
||||||
|
pub created_order: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SetupSlot {
|
||||||
|
pub fn new(title: RoleTitle, created_order: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
created_order,
|
||||||
|
assign_to: None,
|
||||||
|
role: title.into(),
|
||||||
|
modifiers: Vec::new(),
|
||||||
|
slot_id: SlotId::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_character(
|
||||||
|
self,
|
||||||
|
ident: Identification,
|
||||||
|
roles_in_game: &[RoleTitle],
|
||||||
|
) -> Result<Character, GameError> {
|
||||||
|
Character::new(ident.clone(), self.role.into_role(roles_in_game)?)
|
||||||
|
.ok_or(GameError::PlayerNotAssignedNumber(ident.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Category {
|
||||||
|
pub const fn class(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Category::Wolves => "wolves",
|
||||||
|
Category::Villager => "village",
|
||||||
|
Category::Intel => "intel",
|
||||||
|
Category::Defensive => "defensive",
|
||||||
|
Category::Offensive => "offensive",
|
||||||
|
Category::StartsAsVillager => "starts-as-villager",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,44 +26,12 @@ impl Village {
|
||||||
roles: settings.min_players_needed() as u8,
|
roles: settings.min_players_needed() as u8,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
settings.check()?;
|
let mut characters = settings.assign(players)?;
|
||||||
|
assert_eq!(characters.len(), players.len());
|
||||||
|
characters.sort_by_key(|l| l.number());
|
||||||
|
|
||||||
let roles_spread = settings.spread();
|
|
||||||
let potential_apprentice_havers = roles_spread
|
|
||||||
.iter()
|
|
||||||
.filter(|r| r.is_mentor())
|
|
||||||
.map(|r| r.title_to_role_excl_apprentice())
|
|
||||||
.collect::<Box<[_]>>();
|
|
||||||
|
|
||||||
let mut roles = roles_spread
|
|
||||||
.into_iter()
|
|
||||||
.chain(
|
|
||||||
(0..settings.villagers_needed_for_player_count(players.len())?)
|
|
||||||
.map(|_| RoleTitle::Villager),
|
|
||||||
)
|
|
||||||
.map(|title| match title {
|
|
||||||
RoleTitle::Apprentice => Role::Apprentice(Box::new(
|
|
||||||
potential_apprentice_havers
|
|
||||||
[rand::rng().random_range(0..potential_apprentice_havers.len())]
|
|
||||||
.clone(),
|
|
||||||
)),
|
|
||||||
_ => title.title_to_role_excl_apprentice(),
|
|
||||||
})
|
|
||||||
.collect::<Box<[_]>>();
|
|
||||||
|
|
||||||
assert_eq!(players.len(), roles.len());
|
|
||||||
roles.shuffle(&mut rand::rng());
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
characters: players
|
characters,
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.zip(roles)
|
|
||||||
.map(|(player, role)| {
|
|
||||||
let player_str = player.public.to_string();
|
|
||||||
Character::new(player, role)
|
|
||||||
.ok_or(GameError::PlayerNotAssignedNumber(player_str))
|
|
||||||
})
|
|
||||||
.collect::<Result<Box<[_]>>>()?,
|
|
||||||
date_time: DateTime::Night { number: 0 },
|
date_time: DateTime::Night { number: 0 },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -259,17 +227,19 @@ impl RoleTitle {
|
||||||
pub fn title_to_role_excl_apprentice(self) -> Role {
|
pub fn title_to_role_excl_apprentice(self) -> Role {
|
||||||
match self {
|
match self {
|
||||||
RoleTitle::Villager => Role::Villager,
|
RoleTitle::Villager => Role::Villager,
|
||||||
RoleTitle::Scapegoat => Role::Scapegoat,
|
RoleTitle::Scapegoat => Role::Scapegoat {
|
||||||
|
redeemed: rand::random(),
|
||||||
|
},
|
||||||
RoleTitle::Seer => Role::Seer,
|
RoleTitle::Seer => Role::Seer,
|
||||||
RoleTitle::Arcanist => Role::Arcanist,
|
RoleTitle::Arcanist => Role::Arcanist,
|
||||||
RoleTitle::Elder => Role::Elder {
|
RoleTitle::Elder => Role::Elder {
|
||||||
knows_on_night: NonZeroU8::new(rand::rng().random_range(1u8..3)).unwrap(),
|
knows_on_night: NonZeroU8::new(rand::rng().random_range(1u8..3)).unwrap(),
|
||||||
|
has_protection: true,
|
||||||
},
|
},
|
||||||
RoleTitle::Werewolf => Role::Werewolf,
|
RoleTitle::Werewolf => Role::Werewolf,
|
||||||
RoleTitle::AlphaWolf => Role::AlphaWolf { killed: None },
|
RoleTitle::AlphaWolf => Role::AlphaWolf { killed: None },
|
||||||
RoleTitle::DireWolf => Role::DireWolf,
|
RoleTitle::DireWolf => Role::DireWolf,
|
||||||
RoleTitle::Shapeshifter => Role::Shapeshifter { shifted_into: None },
|
RoleTitle::Shapeshifter => Role::Shapeshifter { shifted_into: None },
|
||||||
RoleTitle::Apprentice => panic!("title_to_role_excl_apprentice got an apprentice role"),
|
|
||||||
RoleTitle::Protector => Role::Protector {
|
RoleTitle::Protector => Role::Protector {
|
||||||
last_protected: None,
|
last_protected: None,
|
||||||
},
|
},
|
||||||
|
|
@ -282,6 +252,8 @@ impl RoleTitle {
|
||||||
RoleTitle::Guardian => Role::Guardian {
|
RoleTitle::Guardian => Role::Guardian {
|
||||||
last_protected: None,
|
last_protected: None,
|
||||||
},
|
},
|
||||||
|
// fallback to villager
|
||||||
|
RoleTitle::Apprentice => Role::Villager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ mod night_order;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{Game, GameSettings},
|
game::{Game, GameSettings, SetupRole},
|
||||||
message::{
|
message::{
|
||||||
CharacterState, Identification, PublicIdentity,
|
CharacterState, Identification, PublicIdentity,
|
||||||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||||
|
|
@ -223,9 +223,15 @@ fn starts_with_wolf_intro() {
|
||||||
fn no_wolf_kill_n1() {
|
fn no_wolf_kill_n1() {
|
||||||
let players = gen_players(1..10);
|
let players = gen_players(1..10);
|
||||||
let mut settings = GameSettings::default();
|
let mut settings = GameSettings::default();
|
||||||
settings.add(RoleTitle::Shapeshifter).unwrap();
|
settings.new_slot(RoleTitle::Shapeshifter);
|
||||||
settings.sub(RoleTitle::Werewolf);
|
settings.new_slot(RoleTitle::Protector);
|
||||||
settings.add(RoleTitle::Protector).unwrap();
|
if let Some(slot) = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.find(|s| matches!(s.role, SetupRole::Werewolf))
|
||||||
|
{
|
||||||
|
settings.remove_slot(slot.slot_id);
|
||||||
|
}
|
||||||
let mut game = Game::new(&players, settings).unwrap();
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
|
|
@ -342,9 +348,15 @@ fn protect_stops_shapeshift() {
|
||||||
init_log();
|
init_log();
|
||||||
let players = gen_players(1..10);
|
let players = gen_players(1..10);
|
||||||
let mut settings = GameSettings::default();
|
let mut settings = GameSettings::default();
|
||||||
settings.add(RoleTitle::Shapeshifter).unwrap();
|
settings.new_slot(RoleTitle::Shapeshifter);
|
||||||
settings.sub(RoleTitle::Werewolf);
|
settings.new_slot(RoleTitle::Protector);
|
||||||
settings.add(RoleTitle::Protector).unwrap();
|
if let Some(slot) = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.find(|s| matches!(s.role, SetupRole::Werewolf))
|
||||||
|
{
|
||||||
|
settings.remove_slot(slot.slot_id);
|
||||||
|
}
|
||||||
let mut game = Game::new(&players, settings).unwrap();
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
|
|
@ -484,8 +496,14 @@ fn wolfpack_kill_all_targets_valid() {
|
||||||
init_log();
|
init_log();
|
||||||
let players = gen_players(1..10);
|
let players = gen_players(1..10);
|
||||||
let mut settings = GameSettings::default();
|
let mut settings = GameSettings::default();
|
||||||
settings.add(RoleTitle::Shapeshifter).unwrap();
|
settings.new_slot(RoleTitle::Shapeshifter);
|
||||||
settings.sub(RoleTitle::Werewolf);
|
if let Some(slot) = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.find(|s| matches!(s.role, SetupRole::Werewolf))
|
||||||
|
{
|
||||||
|
settings.remove_slot(slot.slot_id);
|
||||||
|
}
|
||||||
let mut game = Game::new(&players, settings).unwrap();
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
game.r#continue().r#continue();
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
|
@ -549,9 +567,15 @@ fn wolfpack_kill_all_targets_valid() {
|
||||||
fn only_1_shapeshift_prompt_if_first_shifts() {
|
fn only_1_shapeshift_prompt_if_first_shifts() {
|
||||||
let players = gen_players(1..10);
|
let players = gen_players(1..10);
|
||||||
let mut settings = GameSettings::default();
|
let mut settings = GameSettings::default();
|
||||||
settings.add(RoleTitle::Shapeshifter).unwrap();
|
settings.new_slot(RoleTitle::Shapeshifter);
|
||||||
settings.add(RoleTitle::Shapeshifter).unwrap();
|
settings.new_slot(RoleTitle::Shapeshifter);
|
||||||
settings.sub(RoleTitle::Werewolf);
|
if let Some(slot) = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.find(|s| matches!(s.role, SetupRole::Werewolf))
|
||||||
|
{
|
||||||
|
settings.remove_slot(slot.slot_id);
|
||||||
|
}
|
||||||
let mut game = Game::new(&players, settings).unwrap();
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
game.r#continue().r#continue();
|
game.r#continue().r#continue();
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Modifier {
|
pub enum Modifier {
|
||||||
Drunk,
|
Drunk,
|
||||||
Insane,
|
Insane,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
use core::{fmt::Display, num::NonZeroU8};
|
use core::{fmt::Display, num::NonZeroU8};
|
||||||
|
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{DateTime, Village},
|
game::{DateTime, SetupSlot, Village},
|
||||||
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
||||||
modifier::Modifier,
|
modifier::Modifier,
|
||||||
role::{Alignment, MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
|
role::{Alignment, MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
|
||||||
|
|
@ -74,6 +75,13 @@ pub enum KillOutcome {
|
||||||
Failed,
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RoleChange {
|
||||||
|
role: Role,
|
||||||
|
new_role: RoleTitle,
|
||||||
|
changed_on_night: u8,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Character {
|
pub struct Character {
|
||||||
player_id: PlayerId,
|
player_id: PlayerId,
|
||||||
|
|
@ -84,13 +92,6 @@ pub struct Character {
|
||||||
role_changes: Vec<RoleChange>,
|
role_changes: Vec<RoleChange>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct RoleChange {
|
|
||||||
role: Role,
|
|
||||||
new_role: RoleTitle,
|
|
||||||
changed_on_night: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Character {
|
impl Character {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
Identification {
|
Identification {
|
||||||
|
|
@ -119,6 +120,27 @@ impl Character {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn is_power_role(&self) -> bool {
|
||||||
|
match &self.role {
|
||||||
|
Role::Scapegoat { .. } | Role::Villager => false,
|
||||||
|
|
||||||
|
Role::Seer
|
||||||
|
| Role::Arcanist
|
||||||
|
| Role::Gravedigger
|
||||||
|
| Role::Hunter { .. }
|
||||||
|
| Role::Militia { .. }
|
||||||
|
| Role::MapleWolf { .. }
|
||||||
|
| Role::Guardian { .. }
|
||||||
|
| Role::Protector { .. }
|
||||||
|
| Role::Apprentice(..)
|
||||||
|
| Role::Elder { .. }
|
||||||
|
| Role::Werewolf
|
||||||
|
| Role::AlphaWolf { .. }
|
||||||
|
| Role::DireWolf
|
||||||
|
| Role::Shapeshifter { .. } => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn identity(&self) -> CharacterIdentity {
|
pub fn identity(&self) -> CharacterIdentity {
|
||||||
self.identity.clone()
|
self.identity.clone()
|
||||||
}
|
}
|
||||||
|
|
@ -230,8 +252,23 @@ impl Character {
|
||||||
}
|
}
|
||||||
| Role::AlphaWolf { killed: Some(_) }
|
| Role::AlphaWolf { killed: Some(_) }
|
||||||
| Role::Militia { targeted: Some(_) }
|
| Role::Militia { targeted: Some(_) }
|
||||||
| Role::Scapegoat
|
| Role::Scapegoat { redeemed: false }
|
||||||
| Role::Villager => return Ok(None),
|
| Role::Villager => return Ok(None),
|
||||||
|
Role::Scapegoat { redeemed: true } => {
|
||||||
|
let mut dead = village.dead_characters();
|
||||||
|
dead.shuffle(&mut rand::rng());
|
||||||
|
if let Some(pr) = dead
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|d| d.is_power_role().then_some(d.role().title()))
|
||||||
|
{
|
||||||
|
ActionPrompt::RoleChange {
|
||||||
|
character_id: self.identity(),
|
||||||
|
new_role: pr,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
Role::Seer => ActionPrompt::Seer {
|
Role::Seer => ActionPrompt::Seer {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
|
|
@ -264,7 +301,7 @@ impl Character {
|
||||||
return Ok(village
|
return Ok(village
|
||||||
.characters()
|
.characters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|c| c.role().title() == role.title())
|
.filter(|c| c.role().title() == *role)
|
||||||
.filter_map(|char| char.died_to)
|
.filter_map(|char| char.died_to)
|
||||||
.any(|died_to| match died_to.date_time() {
|
.any(|died_to| match died_to.date_time() {
|
||||||
DateTime::Day { number } => number.get() + 1 >= current_night,
|
DateTime::Day { number } => number.get() + 1 >= current_night,
|
||||||
|
|
@ -272,10 +309,10 @@ impl Character {
|
||||||
})
|
})
|
||||||
.then(|| ActionPrompt::RoleChange {
|
.then(|| ActionPrompt::RoleChange {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
new_role: role.title(),
|
new_role: *role,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Role::Elder { knows_on_night } => {
|
Role::Elder { knows_on_night, .. } => {
|
||||||
let current_night = match village.date_time() {
|
let current_night = match village.date_time() {
|
||||||
DateTime::Day { number: _ } => return Ok(None),
|
DateTime::Day { number: _ } => return Ok(None),
|
||||||
DateTime::Night { number } => number,
|
DateTime::Night { number } => number,
|
||||||
|
|
@ -309,11 +346,17 @@ impl Character {
|
||||||
Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter {
|
Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
},
|
},
|
||||||
Role::Gravedigger => ActionPrompt::Gravedigger {
|
Role::Gravedigger => {
|
||||||
|
let dead = village.dead_targets();
|
||||||
|
if dead.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
ActionPrompt::Gravedigger {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
dead_players: village.dead_targets(),
|
dead_players: village.dead_targets(),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}
|
||||||
|
}
|
||||||
Role::Hunter { target } => ActionPrompt::Hunter {
|
Role::Hunter { target } => ActionPrompt::Hunter {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
current_target: target.as_ref().and_then(|t| village.target_by_id(t)),
|
current_target: target.as_ref().and_then(|t| village.target_by_id(t)),
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ pub enum Role {
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks("killer")]
|
#[checks("killer")]
|
||||||
#[checks("powerful")]
|
#[checks("powerful")]
|
||||||
Scapegoat,
|
Scapegoat { redeemed: bool },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks("powerful")]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
|
@ -58,11 +58,14 @@ pub enum Role {
|
||||||
Protector { last_protected: Option<CharacterId> },
|
Protector { last_protected: Option<CharacterId> },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks("powerful")]
|
||||||
Apprentice(Box<Role>),
|
Apprentice(RoleTitle),
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks("powerful")]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Elder { knows_on_night: NonZeroU8 },
|
Elder {
|
||||||
|
knows_on_night: NonZeroU8,
|
||||||
|
has_protection: bool,
|
||||||
|
},
|
||||||
|
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks("killer")]
|
#[checks("killer")]
|
||||||
|
|
@ -90,7 +93,7 @@ impl Role {
|
||||||
/// [RoleTitle] as shown to the player on role assignment
|
/// [RoleTitle] as shown to the player on role assignment
|
||||||
pub const fn initial_shown_role(&self) -> RoleTitle {
|
pub const fn initial_shown_role(&self) -> RoleTitle {
|
||||||
match self {
|
match self {
|
||||||
Role::Apprentice(_) | Role::Elder { knows_on_night: _ } => RoleTitle::Villager,
|
Role::Apprentice(_) | Role::Elder { .. } => RoleTitle::Villager,
|
||||||
_ => self.title(),
|
_ => self.title(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,50 +107,47 @@ impl Role {
|
||||||
return match self {
|
return match self {
|
||||||
Role::DireWolf | Role::Arcanist | Role::Seer => true,
|
Role::DireWolf | Role::Arcanist | Role::Seer => true,
|
||||||
|
|
||||||
Role::Shapeshifter { shifted_into: _ }
|
Role::Shapeshifter { .. }
|
||||||
| Role::Werewolf
|
| Role::Werewolf
|
||||||
| Role::AlphaWolf { killed: _ }
|
| Role::AlphaWolf { .. }
|
||||||
| Role::Elder { knows_on_night: _ }
|
| Role::Elder { .. }
|
||||||
| Role::Gravedigger
|
| Role::Gravedigger
|
||||||
| Role::Hunter { target: _ }
|
| Role::Hunter { .. }
|
||||||
| Role::Militia { targeted: _ }
|
| Role::Militia { .. }
|
||||||
| Role::MapleWolf {
|
| Role::MapleWolf { .. }
|
||||||
last_kill_on_night: _,
|
| Role::Guardian { .. }
|
||||||
}
|
|
||||||
| Role::Guardian { last_protected: _ }
|
|
||||||
| Role::Apprentice(_)
|
| Role::Apprentice(_)
|
||||||
| Role::Villager
|
| Role::Villager
|
||||||
| Role::Scapegoat
|
| Role::Scapegoat { .. }
|
||||||
| Role::Protector { last_protected: _ } => false,
|
| Role::Protector { .. } => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
match self {
|
match self {
|
||||||
Role::AlphaWolf { killed: Some(_) }
|
Role::AlphaWolf { killed: Some(_) }
|
||||||
| Role::Werewolf
|
| Role::Werewolf
|
||||||
| Role::Scapegoat
|
| Role::Scapegoat { redeemed: false }
|
||||||
| Role::Militia { targeted: Some(_) }
|
| Role::Militia { targeted: Some(_) }
|
||||||
| Role::Villager => false,
|
| Role::Villager => false,
|
||||||
|
|
||||||
Role::Shapeshifter { shifted_into: _ }
|
Role::Scapegoat { redeemed: true }
|
||||||
|
| Role::Shapeshifter { .. }
|
||||||
| Role::DireWolf
|
| Role::DireWolf
|
||||||
| Role::AlphaWolf { killed: None }
|
| Role::AlphaWolf { killed: None }
|
||||||
| Role::Arcanist
|
| Role::Arcanist
|
||||||
| Role::Protector { last_protected: _ }
|
| Role::Protector { .. }
|
||||||
| Role::Gravedigger
|
| Role::Gravedigger
|
||||||
| Role::Hunter { target: _ }
|
| Role::Hunter { .. }
|
||||||
| Role::Militia { targeted: None }
|
| Role::Militia { targeted: None }
|
||||||
| Role::MapleWolf {
|
| Role::MapleWolf { .. }
|
||||||
last_kill_on_night: _,
|
| Role::Guardian { .. }
|
||||||
}
|
|
||||||
| Role::Guardian { last_protected: _ }
|
|
||||||
| Role::Seer => true,
|
| Role::Seer => true,
|
||||||
|
|
||||||
Role::Apprentice(role) => village
|
Role::Apprentice(title) => village
|
||||||
.characters()
|
.characters()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|c| c.role().title() == role.title()),
|
.any(|c| c.role().title() == *title),
|
||||||
|
|
||||||
Role::Elder { knows_on_night } => match village.date_time() {
|
Role::Elder { knows_on_night, .. } => match village.date_time() {
|
||||||
DateTime::Night { number } => number == knows_on_night.get(),
|
DateTime::Night { number } => number == knows_on_night.get(),
|
||||||
_ => false,
|
_ => false,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use crate::{
|
||||||
lobby::{Lobby, LobbyPlayers},
|
lobby::{Lobby, LobbyPlayers},
|
||||||
runner::{IdentifiedClientMessage, Message},
|
runner::{IdentifiedClientMessage, Message},
|
||||||
};
|
};
|
||||||
use tokio::{sync::broadcast::Receiver, time::Instant};
|
use tokio::time::Instant;
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{Game, GameOver, Village},
|
game::{Game, GameOver, Village},
|
||||||
|
|
@ -158,8 +158,8 @@ impl GameRunner {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if acks.iter().any(|(c, d)| c.player_id() == &player_id && *d) {
|
if acks.iter().any(|(c, d)| c.player_id() == &player_id && *d) {
|
||||||
// already ack'd just disconnect
|
// already ack'd just sleep
|
||||||
sender.send(ServerMessage::Disconnect).log_debug();
|
sender.send(ServerMessage::Sleep).log_debug();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(char) = self
|
if let Some(char) = self
|
||||||
|
|
@ -196,7 +196,7 @@ impl GameRunner {
|
||||||
}
|
}
|
||||||
(update_host)(&acks, &mut self.comms);
|
(update_host)(&acks, &mut self.comms);
|
||||||
if let Some(sender) = self.joined_players.get_sender(&player_id).await {
|
if let Some(sender) = self.joined_players.get_sender(&player_id).await {
|
||||||
sender.send(ServerMessage::Disconnect).log_debug();
|
sender.send(ServerMessage::Sleep).log_debug();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Client(IdentifiedClientMessage {
|
Message::Client(IdentifiedClientMessage {
|
||||||
|
|
@ -219,7 +219,7 @@ impl GameRunner {
|
||||||
|
|
||||||
for char in self.game.village().characters() {
|
for char in self.game.village().characters() {
|
||||||
if let Some(sender) = self.joined_players.get_sender(char.player_id()).await {
|
if let Some(sender) = self.joined_players.get_sender(char.player_id()).await {
|
||||||
let _ = sender.send(ServerMessage::Disconnect);
|
let _ = sender.send(ServerMessage::Sleep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,6 @@ impl Lobby {
|
||||||
let _ = self.comms().unwrap().host().send(msg);
|
let _ = self.comms().unwrap().host().send(msg);
|
||||||
}
|
}
|
||||||
Message::Host(HostMessage::Lobby(HostLobbyMessage::SetGameSettings(settings))) => {
|
Message::Host(HostMessage::Lobby(HostLobbyMessage::SetGameSettings(settings))) => {
|
||||||
settings.check()?;
|
|
||||||
self.settings = settings;
|
self.settings = settings;
|
||||||
}
|
}
|
||||||
Message::Host(HostMessage::Lobby(HostLobbyMessage::SetPlayerNumber(pid, num))) => {
|
Message::Host(HostMessage::Lobby(HostLobbyMessage::SetPlayerNumber(pid, num))) => {
|
||||||
|
|
@ -187,17 +186,12 @@ impl Lobby {
|
||||||
self.send_lobby_info_to_host().await.log_debug();
|
self.send_lobby_info_to_host().await.log_debug();
|
||||||
}
|
}
|
||||||
Message::Host(HostMessage::Lobby(HostLobbyMessage::Start)) => {
|
Message::Host(HostMessage::Lobby(HostLobbyMessage::Start)) => {
|
||||||
if self.players_in_lobby.len() < self.settings.min_players_needed() {
|
|
||||||
return Err(GameError::TooFewPlayers {
|
|
||||||
got: self.players_in_lobby.len() as _,
|
|
||||||
need: self.settings.min_players_needed() as _,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let playing_players = self
|
let playing_players = self
|
||||||
.players_in_lobby
|
.players_in_lobby
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(id, _)| id.clone())
|
.map(|(id, _)| id.clone())
|
||||||
.collect::<Box<[_]>>();
|
.collect::<Box<[_]>>();
|
||||||
|
self.settings.check_with_player_list(&playing_players)?;
|
||||||
|
|
||||||
let game = Game::new(&playing_players, self.settings.clone())?;
|
let game = Game::new(&playing_players, self.settings.clone())?;
|
||||||
assert_eq!(game.village().characters().len(), playing_players.len());
|
assert_eq!(game.village().characters().len(), playing_players.len());
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,16 @@ $disconnected_color: hsl(0, 68%, 50%);
|
||||||
$client_shadow_color: hsl(260, 55%, 61%);
|
$client_shadow_color: hsl(260, 55%, 61%);
|
||||||
$client_shadow_color_2: hsl(240, 55%, 61%);
|
$client_shadow_color_2: hsl(240, 55%, 61%);
|
||||||
$client_filter: drop-shadow(5px 5px 0 $client_shadow_color) drop-shadow(5px 5px 0 $client_shadow_color_2);
|
$client_filter: drop-shadow(5px 5px 0 $client_shadow_color) drop-shadow(5px 5px 0 $client_shadow_color_2);
|
||||||
|
$village_border: color.change($village_color, $alpha: 1.0);
|
||||||
|
$wolves_border: color.change($wolves_color, $alpha: 1.0);
|
||||||
|
$intel_color: color.adjust($village_color, $hue: -30deg);
|
||||||
|
$intel_border: color.change($intel_color, $alpha: 1.0);
|
||||||
|
$defensive_color: color.adjust($intel_color, $hue: -30deg);
|
||||||
|
$defensive_border: color.change($defensive_color, $alpha: 1.0);
|
||||||
|
$offensive_color: color.adjust($village_color, $hue: 30deg);
|
||||||
|
$offensive_border: color.change($offensive_color, $alpha: 1.0);
|
||||||
|
$starts_as_villager_color: color.adjust($offensive_color, $hue: 30deg);
|
||||||
|
$starts_as_villager_border: color.change($starts_as_villager_color, $alpha: 1.0);
|
||||||
|
|
||||||
|
|
||||||
@mixin flexbox() {
|
@mixin flexbox() {
|
||||||
|
|
@ -112,6 +122,7 @@ nav.debug-nav {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player {
|
.player {
|
||||||
|
|
@ -180,8 +191,9 @@ nav.debug-nav {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
// position: absolute;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
// position: fixed;
|
||||||
|
// left: 0%;
|
||||||
|
|
||||||
// top: 1px;
|
// top: 1px;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
|
|
@ -190,6 +202,11 @@ nav.debug-nav {
|
||||||
& button {
|
& button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&>label {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.click-backdrop {
|
.click-backdrop {
|
||||||
|
|
@ -289,6 +306,12 @@ button {
|
||||||
|
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
|
|
||||||
|
&>p {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wolves-intro {
|
.wolves-intro {
|
||||||
|
|
@ -368,11 +391,6 @@ button.confirm {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.role-card.village {
|
|
||||||
background-color: $village_color;
|
|
||||||
color: rgba(255, 255, 255, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
rolecard {
|
rolecard {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
@ -383,9 +401,7 @@ rolecard {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.role.wolves {
|
|
||||||
background-color: $wolves_color;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool_spacer {
|
bool_spacer {
|
||||||
min-width: 25px;
|
min-width: 25px;
|
||||||
|
|
@ -835,6 +851,10 @@ input {
|
||||||
@extend .column-list;
|
@extend .column-list;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
|
&>.identity {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
&>button {
|
&>button {
|
||||||
width: 90vw;
|
width: 90vw;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
@ -931,3 +951,128 @@ input {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.align-start {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-end {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.increment-decrement {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
|
||||||
|
&>p {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-slot {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
& button label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-role {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.village {
|
||||||
|
background-color: $village_color;
|
||||||
|
border: 1px solid $village_border;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $village_border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wolves {
|
||||||
|
background-color: $wolves_color;
|
||||||
|
border: 1px solid $wolves_border;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $wolves_border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.intel {
|
||||||
|
background-color: $intel_color;
|
||||||
|
border: 1px solid $intel_border;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $intel_border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.defensive {
|
||||||
|
background-color: $defensive_color;
|
||||||
|
border: 1px solid $defensive_border;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $defensive_border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.offensive {
|
||||||
|
background-color: $offensive_color;
|
||||||
|
border: 1px solid $offensive_border;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $offensive_border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.starts-as-villager {
|
||||||
|
background-color: $starts_as_villager_color;
|
||||||
|
border: 1px solid $starts_as_villager_border;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $starts_as_villager_border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignments {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 0.5em;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.assignment {
|
||||||
|
text-align: center;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
border: 1px solid white;
|
||||||
|
|
||||||
|
&>* {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.roles-add-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-around;
|
||||||
|
row-gap: 10px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use yew::prelude::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
clients::client::connection::{Connection2, ConnectionError},
|
clients::client::connection::{Connection2, ConnectionError},
|
||||||
components::{
|
components::{
|
||||||
Button, Identity,
|
Button, CoverOfDarkness, Identity,
|
||||||
client::{ClientNav, Signin},
|
client::{ClientNav, Signin},
|
||||||
},
|
},
|
||||||
storage::StorageKey,
|
storage::StorageKey,
|
||||||
|
|
@ -24,7 +24,7 @@ pub enum ClientEvent2 {
|
||||||
Disconnected,
|
Disconnected,
|
||||||
Connecting,
|
Connecting,
|
||||||
ShowRole(RoleTitle),
|
ShowRole(RoleTitle),
|
||||||
|
Sleep,
|
||||||
Lobby {
|
Lobby {
|
||||||
joined: bool,
|
joined: bool,
|
||||||
players: Rc<[PublicIdentity]>,
|
players: Rc<[PublicIdentity]>,
|
||||||
|
|
@ -79,6 +79,9 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
|
||||||
let connection = use_mut_ref(|| Connection2::new(client_state.setter(), ident.clone(), recv));
|
let connection = use_mut_ref(|| Connection2::new(client_state.setter(), ident.clone(), recv));
|
||||||
|
|
||||||
let content = match &*client_state {
|
let content = match &*client_state {
|
||||||
|
ClientEvent2::Sleep => html! {
|
||||||
|
<CoverOfDarkness />
|
||||||
|
},
|
||||||
ClientEvent2::Disconnected => html! {
|
ClientEvent2::Disconnected => html! {
|
||||||
<div class="column-list">
|
<div class="column-list">
|
||||||
<p>{"disconnected"}</p>
|
<p>{"disconnected"}</p>
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,7 @@ impl Connection2 {
|
||||||
fn message_to_client_state(&self, msg: ServerMessage) -> Option<ClientEvent2> {
|
fn message_to_client_state(&self, msg: ServerMessage) -> Option<ClientEvent2> {
|
||||||
log::debug!("received message: {msg:?}");
|
log::debug!("received message: {msg:?}");
|
||||||
Some(match msg {
|
Some(match msg {
|
||||||
|
ServerMessage::Sleep => ClientEvent2::Sleep,
|
||||||
ServerMessage::Disconnect => ClientEvent2::Disconnected,
|
ServerMessage::Disconnect => ClientEvent2::Disconnected,
|
||||||
ServerMessage::LobbyInfo {
|
ServerMessage::LobbyInfo {
|
||||||
joined,
|
joined,
|
||||||
|
|
@ -245,10 +246,7 @@ impl Connection2 {
|
||||||
self.ident.set((pid, ident));
|
self.ident.set((pid, ident));
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
ServerMessage::GameOver(_)
|
ServerMessage::GameOver(_) | ServerMessage::Reset | ServerMessage::GameInProgress => {
|
||||||
| ServerMessage::Sleep
|
|
||||||
| ServerMessage::Reset
|
|
||||||
| ServerMessage::GameInProgress => {
|
|
||||||
log::info!("ignoring: {msg:?}");
|
log::info!("ignoring: {msg:?}");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,7 @@ async fn worker(mut recv: Receiver<HostMessage>, scope: Scope<Host>) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match parse {
|
match parse {
|
||||||
|
Ok(ServerToHostMessage::Error(GameError::AwaitingResponse)) => {}
|
||||||
Ok(msg) => {
|
Ok(msg) => {
|
||||||
log::debug!("got message: {:?}", msg.title());
|
log::debug!("got message: {:?}", msg.title());
|
||||||
log::trace!("message content: {msg:?}");
|
log::trace!("message content: {msg:?}");
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ pub struct ClickableFieldProps {
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub class: yew::Classes,
|
pub class: yew::Classes,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
|
pub button_class: yew::Classes,
|
||||||
|
#[prop_or_default]
|
||||||
pub with_backdrop_exit: bool,
|
pub with_backdrop_exit: bool,
|
||||||
pub state: UseStateHandle<bool>,
|
pub state: UseStateHandle<bool>,
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +22,7 @@ pub fn ClickableField(
|
||||||
children,
|
children,
|
||||||
options,
|
options,
|
||||||
class,
|
class,
|
||||||
|
button_class,
|
||||||
with_backdrop_exit,
|
with_backdrop_exit,
|
||||||
state,
|
state,
|
||||||
}: &ClickableFieldProps,
|
}: &ClickableFieldProps,
|
||||||
|
|
@ -46,7 +49,9 @@ pub fn ClickableField(
|
||||||
});
|
});
|
||||||
html! {
|
html! {
|
||||||
<div class={class.clone()}>
|
<div class={class.clone()}>
|
||||||
<Button on_click={open_close}>{children.clone()}</Button>
|
<Button on_click={open_close} classes={button_class}>
|
||||||
|
{children.clone()}
|
||||||
|
</Button>
|
||||||
{submenu}
|
{submenu}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,14 @@ use std::{collections::HashMap, rc::Rc};
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use web_sys::HtmlInputElement;
|
use web_sys::HtmlInputElement;
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
error::GameError, game::GameSettings, message::PlayerState, role::RoleTitle,
|
error::GameError,
|
||||||
|
game::{GameSettings, OrRandom, SetupRole, SetupSlot, SlotId},
|
||||||
|
message::{Identification, PlayerState},
|
||||||
|
role::RoleTitle,
|
||||||
};
|
};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
const ALIGN_VILLAGE: &str = "village";
|
use crate::components::{Button, ClickableField, Identity};
|
||||||
const ALIGN_WOLVES: &str = "wolves";
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Properties)]
|
#[derive(Debug, PartialEq, Properties)]
|
||||||
pub struct SettingsProps {
|
pub struct SettingsProps {
|
||||||
|
|
@ -21,173 +23,486 @@ pub struct SettingsProps {
|
||||||
pub on_error: Option<Callback<GameError>>,
|
pub on_error: Option<Callback<GameError>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_role_count(settings: &GameSettings, role: RoleTitle) -> u8 {
|
|
||||||
match role {
|
|
||||||
RoleTitle::Villager => {
|
|
||||||
panic!("villager should not be in the settings page")
|
|
||||||
}
|
|
||||||
_ => settings
|
|
||||||
.roles()
|
|
||||||
.into_iter()
|
|
||||||
.find_map(|(r, cnt)| (r == role).then_some(cnt.get()))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AmountChange {
|
|
||||||
Increment(RoleTitle),
|
|
||||||
Decrement(RoleTitle),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn Settings(props: &SettingsProps) -> Html {
|
pub fn Settings(
|
||||||
let on_update = props.on_update.clone();
|
SettingsProps {
|
||||||
let disabled_reason = match props.settings.check() {
|
settings,
|
||||||
Ok(_) => (props.players_in_lobby.len() < props.settings.min_players_needed())
|
players_in_lobby,
|
||||||
.then(|| String::from("too few players for role setup"))
|
on_update,
|
||||||
.or_else(|| {
|
on_start,
|
||||||
props
|
on_error,
|
||||||
.players_in_lobby
|
}: &SettingsProps,
|
||||||
|
) -> Html {
|
||||||
|
let players = players_in_lobby
|
||||||
.iter()
|
.iter()
|
||||||
.any(|p| p.identification.public.number.is_none())
|
.map(|p| p.identification.clone())
|
||||||
.then(|| String::from("not all players are assigned numbers"))
|
.collect::<Rc<[_]>>();
|
||||||
})
|
let disabled_reason = settings
|
||||||
.or_else(|| {
|
.check_with_player_list(&players)
|
||||||
let mut unique: HashMap<NonZeroU8, NonZeroU8> = HashMap::new();
|
.err()
|
||||||
for p in props.players_in_lobby.iter() {
|
.map(|err| err.to_string());
|
||||||
let num = p.identification.public.number.unwrap();
|
|
||||||
if let Some(cnt) = unique.get_mut(&num) {
|
let roles_in_setup: Rc<[RoleTitle]> = settings
|
||||||
*cnt = NonZeroU8::new(cnt.get() + 1).unwrap();
|
.slots()
|
||||||
} else {
|
|
||||||
unique.insert(num, NonZeroU8::new(1).unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let dupes = unique
|
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(num, cnt)| (cnt.get() > 1).then_some(*num))
|
.map(|s| Into::<RoleTitle>::into(s.role.clone()))
|
||||||
.map(|dupe| dupe.to_string())
|
.collect();
|
||||||
|
let on_update_role = on_update.clone();
|
||||||
|
let settings = Rc::new(settings.clone());
|
||||||
|
let on_update_role_settings = settings.clone();
|
||||||
|
let already_assigned_pids = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|r| r.assign_to.clone())
|
||||||
.collect::<Box<[_]>>();
|
.collect::<Box<[_]>>();
|
||||||
dupes
|
let players_for_assign = players
|
||||||
.is_empty()
|
.iter()
|
||||||
.not()
|
.filter(|p| !already_assigned_pids.contains(&p.player_id))
|
||||||
.then(|| format!("duplicate numbers: {}", dupes.join(", ")))
|
.cloned()
|
||||||
}),
|
.collect::<Rc<[_]>>();
|
||||||
Err(err) => Some(err.to_string()),
|
let update_role_card = Callback::from(move |act: SettingSlotAction| {
|
||||||
};
|
let mut new_settings = (*on_update_role_settings).clone();
|
||||||
let settings = props.settings.clone();
|
match act {
|
||||||
let on_error = props.on_error.clone();
|
SettingSlotAction::Remove(slot_id) => new_settings.remove_slot(slot_id),
|
||||||
let on_changed = Callback::from(move |change: AmountChange| {
|
SettingSlotAction::Update(slot) => new_settings.update_slot(slot),
|
||||||
let mut s = settings.clone();
|
|
||||||
match change {
|
|
||||||
AmountChange::Increment(role) => {
|
|
||||||
if let Err(err) = s.add(role)
|
|
||||||
&& let Some(on_error) = on_error.as_ref()
|
|
||||||
{
|
|
||||||
on_error.emit(err);
|
|
||||||
}
|
}
|
||||||
|
on_update_role.emit(new_settings);
|
||||||
|
});
|
||||||
|
let roles = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.map(|slot| {
|
||||||
|
html! {
|
||||||
|
<SettingsSlot
|
||||||
|
players_for_assign={players_for_assign.clone()}
|
||||||
|
roles_in_setup={roles_in_setup.clone()}
|
||||||
|
slot={slot.clone()}
|
||||||
|
update={update_role_card.clone()}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
AmountChange::Decrement(role) => s.sub(role),
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
|
||||||
|
let add_roles_update = on_update.clone();
|
||||||
|
let add_roles_buttons = RoleTitle::ALL
|
||||||
|
.iter()
|
||||||
|
.map(|r| {
|
||||||
|
let update = add_roles_update.clone();
|
||||||
|
let settings = settings.clone();
|
||||||
|
let role = *r;
|
||||||
|
let on_click = Callback::from(move |_| {
|
||||||
|
let mut settings = (*settings).clone();
|
||||||
|
settings.new_slot(role);
|
||||||
|
update.emit(settings);
|
||||||
|
});
|
||||||
|
let class = Into::<SetupRole>::into(*r).category().class();
|
||||||
|
let name = r.to_string().to_case(Case::Title);
|
||||||
|
html! {
|
||||||
|
<Button on_click={on_click} classes={classes!(class, "add-role")}>
|
||||||
|
{name}
|
||||||
|
</Button>
|
||||||
}
|
}
|
||||||
if s != settings {
|
})
|
||||||
on_update.emit(s)
|
.collect::<Html>();
|
||||||
|
|
||||||
|
let clear_bad_assigned = matches!(
|
||||||
|
settings.check_with_player_list(&players),
|
||||||
|
Err(GameError::AssignedMultipleTimes(_, _)) | Err(GameError::AssignedPlayerMissing(_))
|
||||||
|
)
|
||||||
|
.then(|| {
|
||||||
|
let clear_settings = settings.clone();
|
||||||
|
let clear_update = on_update.clone();
|
||||||
|
let clear_players = players.clone();
|
||||||
|
let clear_bad_assigned = Callback::from(move |_| {
|
||||||
|
let mut settings = (*clear_settings).clone();
|
||||||
|
settings.remove_assignments_not_in_list(&clear_players);
|
||||||
|
settings.remove_duplicate_assignments();
|
||||||
|
clear_update.emit(settings);
|
||||||
|
});
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<Button on_click={clear_bad_assigned}>
|
||||||
|
{"clear bad assignments"}
|
||||||
|
</Button>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let on_start = props.on_start.clone();
|
let clear_all_assignments = settings
|
||||||
let on_start_game = Callback::from(move |_| on_start.emit(()));
|
.slots()
|
||||||
|
.iter()
|
||||||
let roles = RoleTitle::ALL
|
.any(|s| s.assign_to.is_some())
|
||||||
|
.then(|| {
|
||||||
|
let settings = settings.clone();
|
||||||
|
let update = on_update.clone();
|
||||||
|
let on_click = Callback::from(move |_| {
|
||||||
|
let mut settings = (*settings).clone();
|
||||||
|
settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.filter(|s| s.assign_to.is_some())
|
||||||
|
.map(|s| {
|
||||||
|
let mut s = s.clone();
|
||||||
|
s.assign_to.take();
|
||||||
|
s
|
||||||
|
})
|
||||||
|
.collect::<Box<[_]>>()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|r| !matches!(r, RoleTitle::Scapegoat | RoleTitle::Villager))
|
.for_each(|s| settings.update_slot(s));
|
||||||
.map(|r| html! {<RoleCard role={r} amount={get_role_count(&props.settings, r)} on_changed={on_changed.clone()}/>})
|
update.emit(settings);
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<Button on_click={on_click}>{"clear all assignments"}</Button>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let assignments = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.assign_to.is_some())
|
||||||
|
.then(|| {
|
||||||
|
let assignments = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.filter_map(|s| {
|
||||||
|
s.assign_to
|
||||||
|
.as_ref()
|
||||||
|
.map(|a| {
|
||||||
|
players
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.player_id == *a)
|
||||||
|
.map(|assign| {
|
||||||
|
html! {
|
||||||
|
<Identity ident={assign.public.clone()}/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
html! {
|
||||||
|
<span>{"[left the lobby]"}</span>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(|who| {
|
||||||
|
let assignments_update = on_update.clone();
|
||||||
|
let assignments_settings = settings.clone();
|
||||||
|
let click_slot = s.clone();
|
||||||
|
let on_click = Callback::from(move |_| {
|
||||||
|
let mut settings = (*assignments_settings).clone();
|
||||||
|
let mut click_slot = click_slot.clone();
|
||||||
|
click_slot.assign_to.take();
|
||||||
|
settings.update_slot(click_slot);
|
||||||
|
assignments_update.emit(settings);
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<Button classes={classes!("assignment")} on_click={on_click}>
|
||||||
|
<label>{s.role.to_string().to_case(Case::Title)}</label>
|
||||||
|
{who}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
.collect::<Html>();
|
.collect::<Html>();
|
||||||
let disabled = disabled_reason.is_some();
|
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<label>{"assignments"}</label>
|
||||||
|
<div class="assignments">
|
||||||
|
{assignments}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="settings">
|
<div class="settings">
|
||||||
<h2>{format!("Min players for settings: {}", props.settings.min_players_needed())}</h2>
|
<div class="top-settings">
|
||||||
|
{clear_all_assignments}
|
||||||
|
{clear_bad_assigned}
|
||||||
|
</div>
|
||||||
|
{assignments}
|
||||||
|
<p>{format!("min roles for setup: {}", settings.min_players_needed())}</p>
|
||||||
|
<p>{format!("current role count: {}", settings.slots().len())}</p>
|
||||||
|
<div class="roles-add-list">
|
||||||
|
{add_roles_buttons}
|
||||||
|
</div>
|
||||||
<div class="role-list">
|
<div class="role-list">
|
||||||
// <BoolRoleCard role={RoleTitle::Scapegoat} enabled={props.settings.scapegoat} on_changed={on_bool_changed}/>
|
|
||||||
{roles}
|
{roles}
|
||||||
</div>
|
</div>
|
||||||
<button reason={disabled_reason} disabled={disabled} class="start-game" onclick={on_start_game}>{"Start Game"}</button>
|
<Button
|
||||||
|
disabled_reason={disabled_reason}
|
||||||
|
classes={classes!("start-game")}
|
||||||
|
on_click={on_start.clone()}
|
||||||
|
>
|
||||||
|
{"start game"}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BoolRoleSet {
|
pub enum SettingSlotAction {
|
||||||
Scapegoat { enabled: bool },
|
Remove(SlotId),
|
||||||
|
Update(SetupSlot),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Properties)]
|
#[derive(Debug, PartialEq, Properties)]
|
||||||
struct BoolRoleProps {
|
pub struct SettingsSlotProps {
|
||||||
pub role: RoleTitle,
|
pub players_for_assign: Rc<[Identification]>,
|
||||||
pub enabled: bool,
|
pub roles_in_setup: Rc<[RoleTitle]>,
|
||||||
pub on_changed: Callback<BoolRoleSet>,
|
pub slot: SetupSlot,
|
||||||
|
pub update: Callback<SettingSlotAction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
fn BoolRoleCard(props: &BoolRoleProps) -> Html {
|
pub fn SettingsSlot(
|
||||||
let align_class = if props.role.wolf() {
|
SettingsSlotProps {
|
||||||
ALIGN_WOLVES
|
players_for_assign,
|
||||||
} else {
|
roles_in_setup,
|
||||||
ALIGN_VILLAGE
|
slot,
|
||||||
|
update,
|
||||||
|
}: &SettingsSlotProps,
|
||||||
|
) -> Html {
|
||||||
|
let open = use_state(|| false);
|
||||||
|
let open_update = open.setter();
|
||||||
|
let update = update.clone();
|
||||||
|
let update = Callback::from(move |act| {
|
||||||
|
update.emit(act);
|
||||||
|
});
|
||||||
|
let role_name = slot.role.to_string().to_case(Case::Title);
|
||||||
|
let apprentice_open = use_state(|| false);
|
||||||
|
let submenu = {
|
||||||
|
let on_kick_update = update.clone();
|
||||||
|
let slot_id = slot.slot_id;
|
||||||
|
let assign_open = use_state(|| false);
|
||||||
|
let on_kick = Callback::from(move |_| {
|
||||||
|
on_kick_update.emit(SettingSlotAction::Remove(slot_id));
|
||||||
|
open_update.set(false);
|
||||||
|
});
|
||||||
|
let assign_to = assign_to_submenu(players_for_assign, slot, &update, &open.setter());
|
||||||
|
let options =
|
||||||
|
setup_options_for_slot(slot, &update, roles_in_setup, apprentice_open, open.clone());
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<Button on_click={on_kick}>
|
||||||
|
{"remove"}
|
||||||
|
</Button>
|
||||||
|
<ClickableField
|
||||||
|
options={assign_to}
|
||||||
|
state={assign_open}
|
||||||
|
class={classes!("assign-list")}
|
||||||
|
>
|
||||||
|
{"assign"}
|
||||||
|
</ClickableField>
|
||||||
|
{options}
|
||||||
|
</>
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
let class = slot.role.category().class();
|
||||||
|
html! {
|
||||||
|
<ClickableField
|
||||||
|
class={classes!("setup-slot")}
|
||||||
|
button_class={classes!(class)}
|
||||||
|
options={submenu}
|
||||||
|
state={open}
|
||||||
|
with_backdrop_exit=true
|
||||||
|
>
|
||||||
|
<label>{role_name}</label>
|
||||||
|
</ClickableField>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let set_role = match props.role {
|
fn assign_to_submenu(
|
||||||
RoleTitle::Scapegoat => |enabled| BoolRoleSet::Scapegoat { enabled },
|
players: &[Identification],
|
||||||
_ => panic!("invalid role for bool card: {}", props.role),
|
slot: &SetupSlot,
|
||||||
};
|
update: &Callback<SettingSlotAction>,
|
||||||
log::warn!("Role: {} | {};", props.role, props.enabled);
|
assign_setter: &UseStateSetter<bool>,
|
||||||
let enabled = props.enabled;
|
) -> Html {
|
||||||
let role = props.role;
|
players
|
||||||
|
.iter()
|
||||||
let cb = props.on_changed.clone();
|
.map(|p| {
|
||||||
let on_click = Callback::from(move |ev: MouseEvent| {
|
let slot = slot.clone();
|
||||||
let input = ev
|
let update = update.clone();
|
||||||
.target_dyn_into::<HtmlInputElement>()
|
let pid = p.player_id.clone();
|
||||||
.expect("input callback not on input");
|
let setter = assign_setter.clone();
|
||||||
cb.emit(set_role(input.checked()))
|
let on_click = Callback::from(move |_| {
|
||||||
|
let mut slot = slot.clone();
|
||||||
|
slot.assign_to.replace(pid.clone());
|
||||||
|
update.emit(SettingSlotAction::Update(slot));
|
||||||
|
setter.set(false);
|
||||||
});
|
});
|
||||||
html! {
|
html! {
|
||||||
<div class={classes!("role-card", align_class)}>
|
<Button on_click={on_click}>
|
||||||
// <div>
|
<Identity ident={p.public.clone()}/>
|
||||||
<bool_spacer/>
|
</Button>
|
||||||
<bool_role>
|
|
||||||
<role>{role.to_string()}</role>
|
|
||||||
<input onclick={on_click} type="checkbox" checked={enabled}/>
|
|
||||||
</bool_role>
|
|
||||||
// </div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Properties)]
|
fn setup_options_for_slot(
|
||||||
struct RoleProps {
|
slot: &SetupSlot,
|
||||||
pub role: RoleTitle,
|
update: &Callback<SettingSlotAction>,
|
||||||
pub amount: u8,
|
roles_in_setup: &[RoleTitle],
|
||||||
pub on_changed: Callback<AmountChange>,
|
open_apprentice_assign: UseStateHandle<bool>,
|
||||||
}
|
slot_field_open: UseStateHandle<bool>,
|
||||||
|
) -> Html {
|
||||||
#[function_component]
|
let setup_options_for_role = match &slot.role {
|
||||||
fn RoleCard(props: &RoleProps) -> Html {
|
SetupRole::Scapegoat { redeemed } => {
|
||||||
let align_class = if props.role.wolf() {
|
let next = {
|
||||||
ALIGN_WOLVES
|
let next_redeemed = match redeemed {
|
||||||
} else {
|
OrRandom::Random => OrRandom::Determined(true),
|
||||||
ALIGN_VILLAGE
|
OrRandom::Determined(true) => OrRandom::Determined(false),
|
||||||
|
OrRandom::Determined(false) => OrRandom::Random,
|
||||||
};
|
};
|
||||||
let amount = props.amount;
|
let mut s = slot.clone();
|
||||||
let role = props.role;
|
match &mut s.role {
|
||||||
let role_name = role.to_string().to_case(Case::Title);
|
SetupRole::Scapegoat { redeemed } => *redeemed = next_redeemed,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
s
|
||||||
|
};
|
||||||
|
let update = update.clone();
|
||||||
|
let on_click =
|
||||||
|
Callback::from(move |_| update.emit(SettingSlotAction::Update(next.clone())));
|
||||||
|
let body = match redeemed {
|
||||||
|
OrRandom::Determined(true) => "redeemed",
|
||||||
|
OrRandom::Determined(false) => "irredeemable",
|
||||||
|
OrRandom::Random => "random",
|
||||||
|
};
|
||||||
|
Some(html! {
|
||||||
|
<>
|
||||||
|
<label>{"redeemed?"}</label>
|
||||||
|
<Button on_click={on_click}>
|
||||||
|
<label>{body}</label>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
SetupRole::Apprentice { specifically } => {
|
||||||
|
let options = roles_in_setup
|
||||||
|
.iter()
|
||||||
|
.filter(|r| r.is_mentor())
|
||||||
|
.cloned()
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
|
||||||
let cb = props.on_changed.clone();
|
#[allow(clippy::obfuscated_if_else)]
|
||||||
let decrease = Callback::from(move |_| cb.emit(AmountChange::Decrement(role)));
|
options
|
||||||
let cb = props.on_changed.clone();
|
.is_empty()
|
||||||
let increase = Callback::from(move |_| cb.emit(AmountChange::Increment(role)));
|
.not()
|
||||||
|
.then(|| {
|
||||||
|
options
|
||||||
|
.into_iter()
|
||||||
|
.filter(|o| specifically.as_ref().map(|s| s != o).unwrap_or(true))
|
||||||
|
.map(|option| {
|
||||||
|
let open_apprentice_assign = open_apprentice_assign.clone();
|
||||||
|
let open = slot_field_open.clone();
|
||||||
|
let update = update.clone();
|
||||||
|
let role_name = option.to_string().to_case(Case::Title);
|
||||||
|
let mut slot = slot.clone();
|
||||||
|
match &mut slot.role {
|
||||||
|
SetupRole::Apprentice { specifically } => {
|
||||||
|
specifically.replace(option);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
let on_click = Callback::from(move |_| {
|
||||||
|
update.emit(SettingSlotAction::Update(slot.clone()));
|
||||||
|
open_apprentice_assign.set(false);
|
||||||
|
open.set(false);
|
||||||
|
});
|
||||||
html! {
|
html! {
|
||||||
<div class={classes!("role-card", align_class)}>
|
<Button on_click={on_click}>
|
||||||
<button onclick={decrease}>{"-"}</button>
|
{role_name}
|
||||||
<rolecard><role>{role_name}</role><count>{amount.to_string()}</count></rolecard>
|
</Button>
|
||||||
<button onclick={increase}>{"+"}</button>
|
}
|
||||||
|
})
|
||||||
|
.chain(specifically.is_some().then(|| {
|
||||||
|
let open_apprentice_assign = open_apprentice_assign.clone();
|
||||||
|
let open = slot_field_open.clone();
|
||||||
|
let update = update.clone();
|
||||||
|
let role_name = "random";
|
||||||
|
let mut slot = slot.clone();
|
||||||
|
match &mut slot.role {
|
||||||
|
SetupRole::Apprentice { specifically } => {
|
||||||
|
specifically.take();
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
let on_click = Callback::from(move |_| {
|
||||||
|
update.emit(SettingSlotAction::Update(slot.clone()));
|
||||||
|
open_apprentice_assign.set(false);
|
||||||
|
open.set(false);
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<Button on_click={on_click}>
|
||||||
|
{role_name}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.collect::<Html>()
|
||||||
|
})
|
||||||
|
.map(|options| {
|
||||||
|
let current = specifically
|
||||||
|
.as_ref()
|
||||||
|
.map(|r| r.to_string().to_case(Case::Title))
|
||||||
|
.unwrap_or_else(|| String::from("random"));
|
||||||
|
html! {
|
||||||
|
<ClickableField
|
||||||
|
state={open_apprentice_assign}
|
||||||
|
options={options}
|
||||||
|
>
|
||||||
|
{"mentor ("}{current}{")"}
|
||||||
|
</ClickableField>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
SetupRole::Elder { knows_on_night } => {
|
||||||
|
const SAFE_NUM: NonZeroU8 = NonZeroU8::new(1).unwrap();
|
||||||
|
let increment_slot = {
|
||||||
|
let mut s = slot.clone();
|
||||||
|
match &mut s.role {
|
||||||
|
SetupRole::Elder { knows_on_night } => {
|
||||||
|
*knows_on_night =
|
||||||
|
NonZeroU8::new(knows_on_night.get().checked_add(1).unwrap_or(1))
|
||||||
|
.unwrap_or(SAFE_NUM)
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
s
|
||||||
|
};
|
||||||
|
let decrement_slot = {
|
||||||
|
let mut s = slot.clone();
|
||||||
|
match &mut s.role {
|
||||||
|
SetupRole::Elder { knows_on_night } => {
|
||||||
|
*knows_on_night =
|
||||||
|
NonZeroU8::new(knows_on_night.get().checked_sub(1).unwrap_or(u8::MAX))
|
||||||
|
.unwrap_or(SAFE_NUM)
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
s
|
||||||
|
};
|
||||||
|
let update_decrement = update.clone();
|
||||||
|
let decrement = Callback::from(move |_| {
|
||||||
|
update_decrement.emit(SettingSlotAction::Update(decrement_slot.clone()))
|
||||||
|
});
|
||||||
|
let update_increment = update.clone();
|
||||||
|
let increment = Callback::from(move |_| {
|
||||||
|
update_increment.emit(SettingSlotAction::Update(increment_slot.clone()))
|
||||||
|
});
|
||||||
|
Some(html! {
|
||||||
|
<>
|
||||||
|
<label>{"knows on night"}</label>
|
||||||
|
<div class={classes!("increment-decrement")}>
|
||||||
|
<Button on_click={decrement}>{"-"}</Button>
|
||||||
|
<label>{knows_on_night.to_string()}</label>
|
||||||
|
<Button on_click={increment}>{"+"}</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
setup_options_for_role.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
// use core::{
|
||||||
|
// fmt::{Debug, Display},
|
||||||
|
// ops::Not,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// use yew::prelude::*;
|
||||||
|
|
||||||
|
// use crate::components::Button;
|
||||||
|
|
||||||
|
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
// pub struct ToggleIterProps<T, I>
|
||||||
|
// where
|
||||||
|
// T: Debug + PartialEq + Clone + Display + 'static,
|
||||||
|
// I: Iterator<Item = T> + PartialEq + Clone + 'static,
|
||||||
|
// {
|
||||||
|
// pub iter: I,
|
||||||
|
// pub on_update: Callback<T>,
|
||||||
|
// #[prop_or_default]
|
||||||
|
// pub disabled_reason: Option<String>,
|
||||||
|
// #[prop_or_default]
|
||||||
|
// pub classes: yew::Classes,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[function_component]
|
||||||
|
// pub fn ToggleIter<T, I>(
|
||||||
|
// ToggleIterProps {
|
||||||
|
// iter,
|
||||||
|
// on_update,
|
||||||
|
// disabled_reason,
|
||||||
|
// classes,
|
||||||
|
// }: &ToggleIterProps<T, I>,
|
||||||
|
// ) -> Html
|
||||||
|
// where
|
||||||
|
// T: Debug + PartialEq + Clone + Display + 'static,
|
||||||
|
// I: Iterator<Item = T> + PartialEq + Clone + 'static,
|
||||||
|
// {
|
||||||
|
// let mut iter = iter.clone();
|
||||||
|
|
||||||
|
// let value = use_state(|| iter.next().unwrap());
|
||||||
|
// let on_click_value = value.clone();
|
||||||
|
// let on_click = Callback::from(move |_| on_click_value.set(on_click_value.not()));
|
||||||
|
// let children = if **value {
|
||||||
|
// on_true.clone()
|
||||||
|
// } else {
|
||||||
|
// on_false.clone()
|
||||||
|
// };
|
||||||
|
// html! {
|
||||||
|
// <Button
|
||||||
|
// on_click={on_click}
|
||||||
|
// disabled_reason={disabled_reason.clone()}
|
||||||
|
// classes={classes}
|
||||||
|
// >
|
||||||
|
// {children}
|
||||||
|
// </Button>
|
||||||
|
// }
|
||||||
|
// }
|
||||||
Loading…
Reference in New Issue