target merge into identity, self updates, etc

This commit is contained in:
emilis 2025-10-02 17:52:12 +01:00
parent 862c5004fd
commit 01c1a4554a
No known key found for this signature in database
45 changed files with 1480 additions and 1053 deletions

766
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,6 @@ uuid = { version = "1.17", features = ["v4", "serde"] }
rand = { version = "0.9" } rand = { version = "0.9" }
werewolves-macros = { path = "../werewolves-macros" } werewolves-macros = { path = "../werewolves-macros" }
[dev-dependencies] [dev-dependencies]
pretty_assertions = { version = "1" } pretty_assertions = { version = "1" }
pretty_env_logger = { version = "0.5" } pretty_env_logger = { version = "0.5" }

View File

@ -13,8 +13,6 @@ pub enum GameError {
InvalidMessageForGameState, InvalidMessageForGameState,
#[error("no executions during night time")] #[error("no executions during night time")]
NoExecutionsAtNight, NoExecutionsAtNight,
#[error("no-trial not allowed")]
NoTrialNotAllowed,
#[error("chracter is already dead")] #[error("chracter is already dead")]
CharacterAlreadyDead, CharacterAlreadyDead,
#[error("no matching character found")] #[error("no matching character found")]
@ -47,8 +45,6 @@ pub enum GameError {
CantAddVillagerToSettings, 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("BUG: cannot find character in village, but they should be there")]
CannotFindTargetButShouldBeThere,
#[error("inactive game object")] #[error("inactive game object")]
InactiveGameObject, InactiveGameObject,
#[error("socket error: {0}")] #[error("socket error: {0}")]
@ -73,4 +69,6 @@ pub enum GameError {
NoPreviousState, NoPreviousState,
#[error("invalid original kill for guardian guard")] #[error("invalid original kill for guardian guard")]
GuardianInvalidOriginalKill, GuardianInvalidOriginalKill,
#[error("player not assigned number: {0}")]
PlayerNotAssignedNumber(String),
} }

View File

@ -94,8 +94,7 @@ impl Game {
.into_iter() .into_iter()
.map(|c| CharacterState { .map(|c| CharacterState {
player_id: c.player_id().clone(), player_id: c.player_id().clone(),
character_id: c.character_id().clone(), identity: c.identity(),
public_identity: c.public_identity().clone(),
role: c.role().title(), role: c.role().title(),
died_to: c.died_to().cloned(), died_to: c.died_to().cloned(),
}) })
@ -109,9 +108,7 @@ impl Game {
(GameState::Night { night }, HostGameMessage::GetState) => { (GameState::Night { night }, HostGameMessage::GetState) => {
if let Some(res) = night.current_result() { if let Some(res) = night.current_result() {
return Ok(ServerToHostMessage::ActionResult( return Ok(ServerToHostMessage::ActionResult(
night night.current_character().map(|c| c.identity()),
.current_character()
.map(|c| c.public_identity().clone()),
res.clone(), res.clone(),
)); ));
} }
@ -138,9 +135,7 @@ impl Game {
HostGameMessage::Night(HostNightMessage::ActionResponse(resp)), HostGameMessage::Night(HostNightMessage::ActionResponse(resp)),
) => match night.received_response(resp.clone()) { ) => match night.received_response(resp.clone()) {
Ok(res) => Ok(ServerToHostMessage::ActionResult( Ok(res) => Ok(ServerToHostMessage::ActionResult(
night night.current_character().map(|c| c.identity()),
.current_character()
.map(|c| c.public_identity().clone()),
res, res,
)), )),
Err(GameError::NightNeedsNext) => match night.next() { Err(GameError::NightNeedsNext) => match night.next() {

View File

@ -111,7 +111,7 @@ impl Night {
wolves: village wolves: village
.living_wolf_pack_players() .living_wolf_pack_players()
.into_iter() .into_iter()
.map(|w| (w.target(), w.role().title())) .map(|w| (w.identity(), w.role().title()))
.collect(), .collect(),
}); });
} }
@ -324,7 +324,7 @@ impl Night {
.village .village
.character_by_id(&kill_target) .character_by_id(&kill_target)
.ok_or(GameError::NoMatchingCharacterFound)? .ok_or(GameError::NoMatchingCharacterFound)?
.character_identity(), .identity(),
}); });
} }
// Remove any further shapeshift prompts from the queue // Remove any further shapeshift prompts from the queue

View File

@ -7,14 +7,14 @@ use super::Result;
use crate::{ use crate::{
error::GameError, error::GameError,
game::{DateTime, GameOver, GameSettings}, game::{DateTime, GameOver, GameSettings},
message::{Identification, Target}, message::{CharacterIdentity, Identification},
player::{Character, CharacterId, PlayerId}, player::{Character, CharacterId, PlayerId},
role::{Role, RoleTitle}, role::{Role, RoleTitle},
}; };
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Village { pub struct Village {
characters: Vec<Character>, characters: Box<[Character]>,
date_time: DateTime, date_time: DateTime,
} }
@ -58,8 +58,12 @@ impl Village {
.iter() .iter()
.cloned() .cloned()
.zip(roles) .zip(roles)
.map(|(player, role)| Character::new(player, role)) .map(|(player, role)| {
.collect(), let player_str = player.public.to_string();
Character::new(player, role)
.ok_or_else(|| GameError::PlayerNotAssignedNumber(player_str))
})
.collect::<Result<Box<[_]>>>()?,
date_time: DateTime::Night { number: 0 }, date_time: DateTime::Night { number: 0 },
}) })
} }
@ -118,17 +122,11 @@ impl Village {
DateTime::Night { number: _ } => return Err(GameError::NoExecutionsAtNight), DateTime::Night { number: _ } => return Err(GameError::NoExecutionsAtNight),
}; };
if characters.is_empty() {
return Err(GameError::NoTrialNotAllowed);
}
let targets = self let targets = self
.characters .characters
.iter_mut() .iter_mut()
.filter(|c| characters.contains(c.character_id())) .filter(|c| characters.contains(c.character_id()))
.collect::<Box<[_]>>(); .collect::<Box<[_]>>();
if targets.len() != characters.len() {
return Err(GameError::CannotFindTargetButShouldBeThere);
}
for t in targets { for t in targets {
t.execute(day)?; t.execute(day)?;
} }
@ -166,39 +164,39 @@ impl Village {
} }
} }
pub fn living_players(&self) -> Box<[Target]> { pub fn living_players(&self) -> Box<[CharacterIdentity]> {
self.characters self.characters
.iter() .iter()
.filter(|c| c.alive()) .filter(|c| c.alive())
.map(Character::target) .map(Character::identity)
.collect() .collect()
} }
pub fn target_by_id(&self, character_id: &CharacterId) -> Option<Target> { pub fn target_by_id(&self, character_id: &CharacterId) -> Option<CharacterIdentity> {
self.character_by_id(character_id).map(Character::target) self.character_by_id(character_id).map(Character::identity)
} }
pub fn living_villagers(&self) -> Box<[Target]> { pub fn living_villagers(&self) -> Box<[CharacterIdentity]> {
self.characters self.characters
.iter() .iter()
.filter(|c| c.alive() && c.is_village()) .filter(|c| c.alive() && c.is_village())
.map(Character::target) .map(Character::identity)
.collect() .collect()
} }
pub fn living_players_excluding(&self, exclude: &CharacterId) -> Box<[Target]> { pub fn living_players_excluding(&self, exclude: &CharacterId) -> Box<[CharacterIdentity]> {
self.characters self.characters
.iter() .iter()
.filter(|c| c.alive() && c.character_id() != exclude) .filter(|c| c.alive() && c.character_id() != exclude)
.map(Character::target) .map(Character::identity)
.collect() .collect()
} }
pub fn dead_targets(&self) -> Box<[Target]> { pub fn dead_targets(&self) -> Box<[CharacterIdentity]> {
self.characters self.characters
.iter() .iter()
.filter(|c| !c.alive()) .filter(|c| !c.alive())
.map(Character::target) .map(Character::identity)
.collect() .collect()
} }

View File

@ -156,7 +156,7 @@ fn gen_players(range: Range<u8>) -> Box<[Identification]> {
public: PublicIdentity { public: PublicIdentity {
name: format!("player {num}"), name: format!("player {num}"),
pronouns: None, pronouns: None,
number: NonZeroU8::new(num).unwrap(), number: NonZeroU8::new(num),
}, },
}) })
.collect() .collect()

View File

@ -4,7 +4,7 @@ use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
use crate::{ use crate::{
message::{ message::{
CharacterIdentity, PublicIdentity, CharacterIdentity,
night::{ActionPrompt, ActionPromptTitle}, night::{ActionPrompt, ActionPromptTitle},
}, },
player::CharacterId, player::CharacterId,
@ -13,11 +13,9 @@ use crate::{
fn character_identity() -> CharacterIdentity { fn character_identity() -> CharacterIdentity {
CharacterIdentity { CharacterIdentity {
character_id: CharacterId::new(), character_id: CharacterId::new(),
public: PublicIdentity { name: Default::default(),
name: String::new(), pronouns: Default::default(),
pronouns: None,
number: NonZeroU8::new(1).unwrap(), number: NonZeroU8::new(1).unwrap(),
},
} }
} }

View File

@ -14,19 +14,3 @@ pub mod modifier;
pub mod nonzero; pub mod nonzero;
pub mod player; pub mod player;
pub mod role; pub mod role;
#[derive(Debug, Error, Clone, Serialize, Deserialize)]
pub enum MessageError {
#[error("{0}")]
GameError(#[from] GameError),
}
pub(crate) trait MustBeInVillage<T> {
fn must_be_in_village(self) -> Result<T, GameError>;
}
impl<T> MustBeInVillage<T> for Option<T> {
fn must_be_in_village(self) -> Result<T, GameError> {
self.ok_or(GameError::CannotFindTargetButShouldBeThere)
}
}

View File

@ -38,22 +38,6 @@ pub struct DayCharacter {
pub alive: bool, pub alive: bool,
} }
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct Target {
pub character_id: CharacterId,
pub public: PublicIdentity,
}
impl Display for Target {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Target {
character_id,
public,
} = self;
write!(f, "{public} [(c){character_id}]")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ServerMessage { pub enum ServerMessage {
Disconnect, Disconnect,

View File

@ -1,13 +1,12 @@
use core::num::NonZeroU8; use core::num::NonZeroU8;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use werewolves_macros::Extract;
use crate::{ use crate::{
error::GameError, error::GameError,
game::{GameOver, GameSettings}, game::{GameOver, GameSettings},
message::{ message::{
PublicIdentity, Target, CharacterIdentity, PublicIdentity,
night::{ActionPrompt, ActionResponse, ActionResult}, night::{ActionPrompt, ActionResponse, ActionResult},
}, },
player::{CharacterId, PlayerId}, player::{CharacterId, PlayerId},
@ -55,6 +54,7 @@ impl From<HostDayMessage> for HostGameMessage {
pub enum HostLobbyMessage { pub enum HostLobbyMessage {
GetState, GetState,
Kick(PlayerId), Kick(PlayerId),
SetPlayerNumber(PlayerId, NonZeroU8),
GetGameSettings, GetGameSettings,
SetGameSettings(GameSettings), SetGameSettings(GameSettings),
Start, Start,
@ -69,13 +69,13 @@ pub enum ServerToHostMessage {
day: NonZeroU8, day: NonZeroU8,
}, },
ActionPrompt(ActionPrompt), ActionPrompt(ActionPrompt),
ActionResult(Option<PublicIdentity>, ActionResult), ActionResult(Option<CharacterIdentity>, ActionResult),
Lobby(Box<[PlayerState]>), Lobby(Box<[PlayerState]>),
GameSettings(GameSettings), GameSettings(GameSettings),
Error(GameError), Error(GameError),
GameOver(GameOver), GameOver(GameOver),
WaitingForRoleRevealAcks { WaitingForRoleRevealAcks {
ackd: Box<[Target]>, ackd: Box<[CharacterIdentity]>,
waiting: Box<[Target]>, waiting: Box<[CharacterIdentity]>,
}, },
} }

View File

@ -14,34 +14,70 @@ pub struct Identification {
pub public: PublicIdentity, pub public: PublicIdentity,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct PublicIdentity { pub struct PublicIdentity {
pub name: String, pub name: String,
pub pronouns: Option<String>, pub pronouns: Option<String>,
pub number: NonZeroU8, pub number: Option<NonZeroU8>,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CharacterIdentity { pub struct CharacterIdentity {
pub character_id: CharacterId, pub character_id: CharacterId,
pub public: PublicIdentity, pub name: String,
pub pronouns: Option<String>,
pub number: NonZeroU8,
}
impl From<CharacterIdentity> for PublicIdentity {
fn from(c: CharacterIdentity) -> Self {
Self {
name: c.name,
pronouns: c.pronouns,
number: Some(c.number),
}
}
}
impl From<&CharacterIdentity> for PublicIdentity {
fn from(c: &CharacterIdentity) -> Self {
Self {
name: c.name.clone(),
pronouns: c.pronouns.clone(),
number: Some(c.number),
}
}
}
impl Display for CharacterIdentity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let CharacterIdentity {
character_id,
name,
pronouns,
number,
} = self;
let pronouns = pronouns
.as_ref()
.map(|p| format!(" ({p})"))
.unwrap_or_default();
write!(f, "[{number}] {name}{pronouns} <<{character_id}>>")
}
} }
impl CharacterIdentity { impl CharacterIdentity {
pub const fn new(character_id: CharacterId, public: PublicIdentity) -> Self { pub const fn new(
character_id: CharacterId,
name: String,
pronouns: Option<String>,
number: NonZeroU8,
) -> Self {
Self { Self {
name,
number,
pronouns,
character_id, character_id,
public,
}
}
}
impl Default for PublicIdentity {
fn default() -> Self {
Self {
name: Default::default(),
pronouns: Default::default(),
number: NonZeroU8::new(1).unwrap(),
} }
} }
} }
@ -57,7 +93,11 @@ impl Display for PublicIdentity {
.as_ref() .as_ref()
.map(|p| format!(" ({p})")) .map(|p| format!(" ({p})"))
.unwrap_or_default(); .unwrap_or_default();
write!(f, "[{number}] {name}{pronouns}") let number = number
.as_ref()
.map(|n| format!("[{n}] "))
.unwrap_or_default();
write!(f, "{number}{name}{pronouns}")
} }
} }
@ -83,8 +123,7 @@ pub struct PlayerState {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CharacterState { pub struct CharacterState {
pub player_id: PlayerId, pub player_id: PlayerId,
pub character_id: CharacterId, pub identity: CharacterIdentity,
pub public_identity: PublicIdentity,
pub role: RoleTitle, pub role: RoleTitle,
pub died_to: Option<DiedTo>, pub died_to: Option<DiedTo>,
} }

View File

@ -7,8 +7,6 @@ use crate::{
role::{Alignment, PreviousGuardianAction, RoleTitle}, role::{Alignment, PreviousGuardianAction, RoleTitle},
}; };
use super::Target;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
pub enum ActionType { pub enum ActionType {
Cover, Cover,
@ -40,7 +38,9 @@ pub enum ActionPrompt {
CoverOfDarkness, CoverOfDarkness,
#[checks(ActionType::WolfPackKill)] #[checks(ActionType::WolfPackKill)]
#[checks] #[checks]
WolvesIntro { wolves: Box<[(Target, RoleTitle)]> }, WolvesIntro {
wolves: Box<[(CharacterIdentity, RoleTitle)]>,
},
#[checks(ActionType::RoleChange)] #[checks(ActionType::RoleChange)]
RoleChange { RoleChange {
character_id: CharacterIdentity, character_id: CharacterIdentity,
@ -49,59 +49,61 @@ pub enum ActionPrompt {
#[checks(ActionType::Other)] #[checks(ActionType::Other)]
Seer { Seer {
character_id: CharacterIdentity, character_id: CharacterIdentity,
living_players: Box<[Target]>, living_players: Box<[CharacterIdentity]>,
}, },
#[checks(ActionType::Protect)] #[checks(ActionType::Protect)]
Protector { Protector {
character_id: CharacterIdentity, character_id: CharacterIdentity,
targets: Box<[Target]>, targets: Box<[CharacterIdentity]>,
}, },
#[checks(ActionType::Other)] #[checks(ActionType::Other)]
Arcanist { Arcanist {
character_id: CharacterIdentity, character_id: CharacterIdentity,
living_players: Box<[Target]>, living_players: Box<[CharacterIdentity]>,
}, },
#[checks(ActionType::Other)] #[checks(ActionType::Other)]
Gravedigger { Gravedigger {
character_id: CharacterIdentity, character_id: CharacterIdentity,
dead_players: Box<[Target]>, dead_players: Box<[CharacterIdentity]>,
}, },
#[checks(ActionType::Other)] #[checks(ActionType::Other)]
Hunter { Hunter {
character_id: CharacterIdentity, character_id: CharacterIdentity,
current_target: Option<Target>, current_target: Option<CharacterIdentity>,
living_players: Box<[Target]>, living_players: Box<[CharacterIdentity]>,
}, },
#[checks(ActionType::Other)] #[checks(ActionType::Other)]
Militia { Militia {
character_id: CharacterIdentity, character_id: CharacterIdentity,
living_players: Box<[Target]>, living_players: Box<[CharacterIdentity]>,
}, },
#[checks(ActionType::Other)] #[checks(ActionType::Other)]
MapleWolf { MapleWolf {
character_id: CharacterIdentity, character_id: CharacterIdentity,
kill_or_die: bool, kill_or_die: bool,
living_players: Box<[Target]>, living_players: Box<[CharacterIdentity]>,
}, },
#[checks(ActionType::Protect)] #[checks(ActionType::Protect)]
Guardian { Guardian {
character_id: CharacterIdentity, character_id: CharacterIdentity,
previous: Option<PreviousGuardianAction>, previous: Option<PreviousGuardianAction>,
living_players: Box<[Target]>, living_players: Box<[CharacterIdentity]>,
}, },
#[checks(ActionType::WolfPackKill)] #[checks(ActionType::WolfPackKill)]
WolfPackKill { living_villagers: Box<[Target]> }, WolfPackKill {
living_villagers: Box<[CharacterIdentity]>,
},
#[checks(ActionType::OtherWolf)] #[checks(ActionType::OtherWolf)]
Shapeshifter { character_id: CharacterIdentity }, Shapeshifter { character_id: CharacterIdentity },
#[checks(ActionType::OtherWolf)] #[checks(ActionType::OtherWolf)]
AlphaWolf { AlphaWolf {
character_id: CharacterIdentity, character_id: CharacterIdentity,
living_villagers: Box<[Target]>, living_villagers: Box<[CharacterIdentity]>,
}, },
#[checks(ActionType::Direwolf)] #[checks(ActionType::Direwolf)]
DireWolf { DireWolf {
character_id: CharacterIdentity, character_id: CharacterIdentity,
living_players: Box<[Target]>, living_players: Box<[CharacterIdentity]>,
}, },
} }

View File

@ -6,7 +6,7 @@ use crate::{
diedto::DiedTo, diedto::DiedTo,
error::GameError, error::GameError,
game::{DateTime, Village}, game::{DateTime, Village},
message::{CharacterIdentity, Identification, PublicIdentity, Target, night::ActionPrompt}, message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
modifier::Modifier, modifier::Modifier,
role::{MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle}, role::{MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
}; };
@ -77,8 +77,7 @@ pub enum KillOutcome {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Character { pub struct Character {
player_id: PlayerId, player_id: PlayerId,
character_id: CharacterId, identity: CharacterIdentity,
public: PublicIdentity,
role: Role, role: Role,
modifier: Option<Modifier>, modifier: Option<Modifier>,
died_to: Option<DiedTo>, died_to: Option<DiedTo>,
@ -93,46 +92,47 @@ pub struct RoleChange {
} }
impl Character { impl Character {
pub fn new(Identification { player_id, public }: Identification, role: Role) -> Self { pub fn new(
Self { Identification {
role,
public,
player_id, player_id,
public:
PublicIdentity {
name,
pronouns,
number,
},
}: Identification,
role: Role,
) -> Option<Self> {
Some(Self {
role,
identity: CharacterIdentity {
character_id: CharacterId::new(), character_id: CharacterId::new(),
name,
pronouns,
number: number?,
},
player_id,
modifier: None, modifier: None,
died_to: None, died_to: None,
role_changes: Vec::new(), role_changes: Vec::new(),
} })
} }
pub fn target(&self) -> Target { pub fn identity(&self) -> CharacterIdentity {
Target { self.identity.clone()
character_id: self.character_id.clone(),
public: self.public.clone(),
}
}
pub const fn public_identity(&self) -> &PublicIdentity {
&self.public
}
pub fn character_identity(&self) -> CharacterIdentity {
CharacterIdentity {
character_id: self.character_id.clone(),
public: self.public.clone(),
}
} }
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
&self.public.name self.identity.name.as_str()
} }
pub const fn number(&self) -> NonZeroU8 { pub const fn number(&self) -> NonZeroU8 {
self.public.number self.identity.number
} }
pub const fn pronouns(&self) -> Option<&str> { pub const fn pronouns(&self) -> Option<&str> {
match self.public.pronouns.as_ref() { match self.identity.pronouns.as_ref() {
Some(p) => Some(p.as_str()), Some(p) => Some(p.as_str()),
None => None, None => None,
} }
@ -162,7 +162,7 @@ impl Character {
} }
pub const fn character_id(&self) -> &CharacterId { pub const fn character_id(&self) -> &CharacterId {
&self.character_id &self.identity.character_id
} }
pub const fn player_id(&self) -> &PlayerId { pub const fn player_id(&self) -> &PlayerId {
@ -220,24 +220,24 @@ impl Character {
| Role::Scapegoat | Role::Scapegoat
| Role::Villager => return Ok(None), | Role::Villager => return Ok(None),
Role::Seer => ActionPrompt::Seer { Role::Seer => ActionPrompt::Seer {
character_id: self.character_identity(), character_id: self.identity(),
living_players: village.living_players_excluding(&self.character_id), living_players: village.living_players_excluding(self.character_id()),
}, },
Role::Arcanist => ActionPrompt::Arcanist { Role::Arcanist => ActionPrompt::Arcanist {
character_id: self.character_identity(), character_id: self.identity(),
living_players: village.living_players_excluding(&self.character_id), living_players: village.living_players_excluding(self.character_id()),
}, },
Role::Protector { Role::Protector {
last_protected: Some(last_protected), last_protected: Some(last_protected),
} => ActionPrompt::Protector { } => ActionPrompt::Protector {
character_id: self.character_identity(), character_id: self.identity(),
targets: village.living_players_excluding(last_protected), targets: village.living_players_excluding(last_protected),
}, },
Role::Protector { Role::Protector {
last_protected: None, last_protected: None,
} => ActionPrompt::Protector { } => ActionPrompt::Protector {
character_id: self.character_identity(), character_id: self.identity(),
targets: village.living_players_excluding(&self.character_id), targets: village.living_players_excluding(self.character_id()),
}, },
Role::Apprentice(role) => { Role::Apprentice(role) => {
let current_night = match village.date_time() { let current_night = match village.date_time() {
@ -254,7 +254,7 @@ impl Character {
DateTime::Night { number } => number + 1 >= current_night, DateTime::Night { number } => number + 1 >= current_night,
}) })
.then(|| ActionPrompt::RoleChange { .then(|| ActionPrompt::RoleChange {
character_id: self.character_identity(), character_id: self.identity(),
new_role: role.title(), new_role: role.title(),
})); }));
} }
@ -265,61 +265,61 @@ impl Character {
}; };
return Ok((current_night == knows_on_night.get()).then_some({ return Ok((current_night == knows_on_night.get()).then_some({
ActionPrompt::RoleChange { ActionPrompt::RoleChange {
character_id: self.character_identity(), character_id: self.identity(),
new_role: RoleTitle::Elder, new_role: RoleTitle::Elder,
} }
})); }));
} }
Role::Militia { targeted: None } => ActionPrompt::Militia { Role::Militia { targeted: None } => ActionPrompt::Militia {
character_id: self.character_identity(), character_id: self.identity(),
living_players: village.living_players_excluding(&self.character_id), living_players: village.living_players_excluding(self.character_id()),
}, },
Role::Werewolf => ActionPrompt::WolfPackKill { Role::Werewolf => ActionPrompt::WolfPackKill {
living_villagers: village.living_players(), living_villagers: village.living_players(),
}, },
Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf { Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf {
character_id: self.character_identity(), character_id: self.identity(),
living_villagers: village.living_players_excluding(&self.character_id), living_villagers: village.living_players_excluding(self.character_id()),
}, },
Role::DireWolf => ActionPrompt::DireWolf { Role::DireWolf => ActionPrompt::DireWolf {
character_id: self.character_identity(), character_id: self.identity(),
living_players: village.living_players(), living_players: village.living_players(),
}, },
Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter { Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter {
character_id: self.character_identity(), character_id: self.identity(),
}, },
Role::Gravedigger => ActionPrompt::Gravedigger { Role::Gravedigger => ActionPrompt::Gravedigger {
character_id: self.character_identity(), character_id: self.identity(),
dead_players: village.dead_targets(), dead_players: village.dead_targets(),
}, },
Role::Hunter { target } => ActionPrompt::Hunter { Role::Hunter { target } => ActionPrompt::Hunter {
character_id: self.character_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)),
living_players: village.living_players_excluding(&self.character_id), living_players: village.living_players_excluding(self.character_id()),
}, },
Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf { Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf {
character_id: self.character_identity(), character_id: self.identity(),
kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night, kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night,
living_players: village.living_players_excluding(&self.character_id), living_players: village.living_players_excluding(self.character_id()),
}, },
Role::Guardian { Role::Guardian {
last_protected: Some(PreviousGuardianAction::Guard(prev_target)), last_protected: Some(PreviousGuardianAction::Guard(prev_target)),
} => ActionPrompt::Guardian { } => ActionPrompt::Guardian {
character_id: self.character_identity(), character_id: self.identity(),
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())), previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
living_players: village.living_players_excluding(&prev_target.character_id), living_players: village.living_players_excluding(&prev_target.character_id),
}, },
Role::Guardian { Role::Guardian {
last_protected: Some(PreviousGuardianAction::Protect(prev_target)), last_protected: Some(PreviousGuardianAction::Protect(prev_target)),
} => ActionPrompt::Guardian { } => ActionPrompt::Guardian {
character_id: self.character_identity(), character_id: self.identity(),
previous: Some(PreviousGuardianAction::Protect(prev_target.clone())), previous: Some(PreviousGuardianAction::Protect(prev_target.clone())),
living_players: village.living_players(), living_players: village.living_players(),
}, },
Role::Guardian { Role::Guardian {
last_protected: None, last_protected: None,
} => ActionPrompt::Guardian { } => ActionPrompt::Guardian {
character_id: self.character_identity(), character_id: self.identity(),
previous: None, previous: None,
living_players: village.living_players(), living_players: village.living_players(),
}, },

View File

@ -5,7 +5,7 @@ use werewolves_macros::{ChecksAs, Titles};
use crate::{ use crate::{
game::{DateTime, Village}, game::{DateTime, Village},
message::Target, message::CharacterIdentity,
player::CharacterId, player::CharacterId,
}; };
@ -178,6 +178,6 @@ pub enum RoleBlock {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum PreviousGuardianAction { pub enum PreviousGuardianAction {
Protect(Target), Protect(CharacterIdentity),
Guard(Target), Guard(CharacterIdentity),
} }

View File

@ -5,7 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
axum = { version = "0.8", features = ["ws"] } axum = { version = "0.8", features = ["ws"] }
tokio = { version = "1.44", features = ["full"] } tokio = { version = "1.47", features = ["full"] }
log = { version = "0.4" } log = { version = "0.4" }
pretty_env_logger = { version = "0.5" } pretty_env_logger = { version = "0.5" }
# env_logger = { version = "0.11" } # env_logger = { version = "0.11" }

View File

@ -121,7 +121,7 @@ struct Client {
who: String, who: String,
sender: Sender<IdentifiedClientMessage>, sender: Sender<IdentifiedClientMessage>,
receiver: Receiver<ServerMessage>, receiver: Receiver<ServerMessage>,
message_history: Vec<ServerMessage>, // message_history: Vec<ServerMessage>,
} }
impl Client { impl Client {
@ -140,7 +140,7 @@ impl Client {
who, who,
sender, sender,
receiver, receiver,
message_history: Vec::new(), // message_history: Vec::new(),
} }
} }
#[cfg(feature = "cbor")] #[cfg(feature = "cbor")]
@ -193,7 +193,7 @@ impl Client {
if let ClientMessage::UpdateSelf(update) = &message { if let ClientMessage::UpdateSelf(update) = &message {
match update { match update {
UpdateSelf::Name(name) => self.ident.public.name = name.clone(), UpdateSelf::Name(name) => self.ident.public.name = name.clone(),
UpdateSelf::Number(num) => self.ident.public.number = *num, UpdateSelf::Number(num) => self.ident.public.number = Some(*num),
UpdateSelf::Pronouns(pronouns) => self.ident.public.pronouns = pronouns.clone(), UpdateSelf::Pronouns(pronouns) => self.ident.public.pronouns = pronouns.clone(),
} }
} }
@ -221,7 +221,7 @@ impl Client {
}) })
}) })
.await?; .await?;
self.message_history.push(message); // self.message_history.push(message);
Ok(()) Ok(())
} }

View File

@ -1,35 +1,25 @@
use core::time::Duration;
use std::collections::HashMap;
use colored::Colorize; use colored::Colorize;
use tokio::{ use tokio::sync::broadcast::Receiver;
sync::broadcast::{Receiver, Sender}, use werewolves_proto::{error::GameError, player::PlayerId};
time::Instant,
};
use werewolves_proto::{
error::GameError,
message::{ClientMessage, ServerMessage, Target, night::ActionResponse},
player::{Character, CharacterId, PlayerId},
};
use crate::{connection::JoinedPlayers, runner::IdentifiedClientMessage}; use crate::{connection::JoinedPlayers, runner::IdentifiedClientMessage};
pub struct PlayerIdComms { pub struct PlayerIdComms {
joined_players: JoinedPlayers, // joined_players: JoinedPlayers,
message_recv: Receiver<IdentifiedClientMessage>, message_recv: Receiver<IdentifiedClientMessage>,
connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>, // connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>,
} }
impl PlayerIdComms { impl PlayerIdComms {
pub fn new( pub fn new(
joined_players: JoinedPlayers, // joined_players: JoinedPlayers,
message_recv: Receiver<IdentifiedClientMessage>, message_recv: Receiver<IdentifiedClientMessage>,
connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>, // connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>,
) -> Self { ) -> Self {
Self { Self {
joined_players, // joined_players,
message_recv, message_recv,
connect_recv, // connect_recv,
} }
} }

View File

@ -36,7 +36,7 @@ pub struct JoinedPlayer {
active_connection: ConnectionId, active_connection: ConnectionId,
in_game: bool, in_game: bool,
pub name: String, pub name: String,
pub number: NonZeroU8, pub number: Option<NonZeroU8>,
pub pronouns: Option<String>, pub pronouns: Option<String>,
} }
@ -46,7 +46,7 @@ impl JoinedPlayer {
receiver: Receiver<ServerMessage>, receiver: Receiver<ServerMessage>,
active_connection: ConnectionId, active_connection: ConnectionId,
name: String, name: String,
number: NonZeroU8, number: Option<NonZeroU8>,
pronouns: Option<String>, pronouns: Option<String>,
) -> Self { ) -> Self {
Self { Self {
@ -111,6 +111,16 @@ impl JoinedPlayers {
} }
} }
pub async fn get_player_identity(&self, player_id: &PlayerId) -> Option<PublicIdentity> {
self.players.lock().await.iter().find_map(|(id, p)| {
(id == player_id).then(|| PublicIdentity {
name: p.name.clone(),
pronouns: p.pronouns.clone(),
number: p.number,
})
})
}
/// Disconnect the player /// Disconnect the player
/// ///
/// Will not disconnect if the player is currently in a game, allowing them to reconnect /// Will not disconnect if the player is currently in a game, allowing them to reconnect

View File

@ -3,7 +3,7 @@ use core::ops::Not;
use crate::{ use crate::{
LogError, LogError,
communication::{Comms, lobby::LobbyComms}, communication::{Comms, lobby::LobbyComms},
connection::{InGameToken, JoinedPlayers}, connection::JoinedPlayers,
lobby::{Lobby, LobbyPlayers}, lobby::{Lobby, LobbyPlayers},
runner::{IdentifiedClientMessage, Message}, runner::{IdentifiedClientMessage, Message},
}; };
@ -13,7 +13,7 @@ use werewolves_proto::{
game::{Game, GameOver, Village}, game::{Game, GameOver, Village},
message::{ message::{
ClientMessage, Identification, ServerMessage, ClientMessage, Identification, ServerMessage,
host::{HostGameMessage, HostMessage, HostNightMessage, ServerToHostMessage}, host::{HostGameMessage, HostMessage, ServerToHostMessage},
}, },
player::{Character, PlayerId}, player::{Character, PlayerId},
}; };
@ -94,11 +94,11 @@ impl GameRunner {
.send(ServerToHostMessage::WaitingForRoleRevealAcks { .send(ServerToHostMessage::WaitingForRoleRevealAcks {
ackd: acks ackd: acks
.iter() .iter()
.filter_map(|(a, ackd)| ackd.then_some(a.target())) .filter_map(|(a, ackd)| ackd.then_some(a.identity()))
.collect(), .collect(),
waiting: acks waiting: acks
.iter() .iter()
.filter_map(|(a, ackd)| ackd.not().then_some(a.target())) .filter_map(|(a, ackd)| ackd.not().then_some(a.identity()))
.collect(), .collect(),
}) })
.log_err(); .log_err();

View File

@ -3,7 +3,7 @@ use core::{
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
time::Duration, time::Duration,
}; };
use std::collections::HashMap; use std::{collections::HashMap, os::unix::raw::pid_t};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::broadcast::Sender; use tokio::sync::broadcast::Sender;
@ -178,6 +178,21 @@ impl Lobby {
settings.check()?; settings.check()?;
self.settings = settings; self.settings = settings;
} }
Message::Host(HostMessage::Lobby(HostLobbyMessage::SetPlayerNumber(pid, num))) => {
self.joined_players
.update(&pid, |p| p.number = Some(num))
.await;
if let Some(p) = self
.players_in_lobby
.iter_mut()
.find_map(|(c, _)| (c.player_id == pid).then_some(c))
&& let Some(joined_id) = self.joined_players.get_player_identity(&pid).await
{
p.public = joined_id;
}
self.send_lobby_info_to_clients().await;
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() { if self.players_in_lobby.len() < self.settings.min_players_needed() {
return Err(GameError::TooFewPlayers { return Err(GameError::TooFewPlayers {
@ -275,12 +290,19 @@ impl Lobby {
message: ClientMessage::UpdateSelf(_), message: ClientMessage::UpdateSelf(_),
}) => { }) => {
self.joined_players self.joined_players
.update(&player_id, move |p| { .update(&player_id, |p| {
p.name = public.name; p.name = public.name.clone();
p.number = public.number; p.number = public.number;
p.pronouns = public.pronouns; p.pronouns = public.pronouns.clone();
}) })
.await; .await;
if let Some(p) = self
.players_in_lobby
.iter_mut()
.find_map(|(c, _)| (c.player_id == player_id).then_some(c))
{
p.public = public;
}
self.send_lobby_info_to_clients().await; self.send_lobby_info_to_clients().await;
self.send_lobby_info_to_host().await.log_debug(); self.send_lobby_info_to_host().await.log_debug();
} }

View File

@ -101,7 +101,11 @@ async fn main() {
let lobby_comms = LobbyComms::new( let lobby_comms = LobbyComms::new(
Comms::new( Comms::new(
HostComms::new(server_send, server_recv), HostComms::new(server_send, server_recv),
PlayerIdComms::new(joined_players.clone(), recv, connect_recv.resubscribe()), PlayerIdComms::new(
//joined_players.clone(),
recv,
// connect_recv.resubscribe()
),
), ),
connect_recv, connect_recv,
); );

View File

@ -14,12 +14,13 @@ web-sys = { version = "0.3", features = [
"HtmlDivElement", "HtmlDivElement",
"HtmlSelectElement", "HtmlSelectElement",
] } ] }
wasm-bindgen = { version = "=0.2.100" }
log = "0.4" log = "0.4"
rand = { version = "0.9", features = ["small_rng"] } rand = { version = "0.9", features = ["small_rng"] }
getrandom = { version = "0.3", features = ["wasm_js"] } getrandom = { version = "0.3", features = ["wasm_js"] }
uuid = { version = "*", features = ["js"] } uuid = { version = "*", features = ["js"] }
yew = { version = "0.21", features = ["csr"] } yew = { version = "0.21", features = ["csr"] }
yew-router = "0.18.0" yew-router = "0.18"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", optional = true } serde_json = { version = "1.0", optional = true }
gloo = "0.11" gloo = "0.11"
@ -29,8 +30,8 @@ once_cell = "1"
chrono = { version = "0.4" } chrono = { version = "0.4" }
werewolves-macros = { path = "../werewolves-macros" } werewolves-macros = { path = "../werewolves-macros" }
werewolves-proto = { path = "../werewolves-proto" } werewolves-proto = { path = "../werewolves-proto" }
futures = "0.3.31" futures = "0.3"
wasm-bindgen-futures = "0.4.50" wasm-bindgen-futures = "0.4"
thiserror = { version = "2" } thiserror = { version = "2" }
convert_case = { version = "0.8" } convert_case = { version = "0.8" }
ciborium = { version = "0.2", optional = true } ciborium = { version = "0.2", optional = true }

View File

@ -14,12 +14,12 @@
<body> <body>
<app></app> <app></app>
<clients> <clients>
<dupe1></dupe1> <!-- <dupe1></dupe1>
<dupe2></dupe2> <dupe2></dupe2>
<dupe3></dupe3> <dupe3></dupe3>
<dupe4></dupe4> <dupe4></dupe4>
<dupe5></dupe5> <dupe5></dupe5>
<dupe6></dupe6> <dupe6></dupe6> -->
</clients> </clients>
<error></error> <error></error>
</body> </body>

View File

@ -54,43 +54,86 @@ nav.debug-nav {
} }
.button-container { .default-button {
button {
font-size: 1.3rem; font-size: 1.3rem;
border: 1px solid rgba(255, 255, 255, 1); border: 1px solid rgba(255, 255, 255, 1);
padding: 5px; padding: 5px;
background-color: rgba(0, 0, 0, 0.3); background-color: black;
color: #cccccc; color: #cccccc;
cursor: pointer; cursor: pointer;
&:hover {
background-color: white;
color: invert(#cccccc);
} }
button:hover {
filter: $link_select_filter;
}
// button {
// color: #fff;
// background: transparent;
// background-repeat: no-repeat;
// cursor: pointer;
// overflow: hidden;
// outline: none;
// padding: 0px;
// &:hover {
// background-color: rgba(0, 0, 0, 0.5);
// }
// }
} }
.player .number { .player {
margin: 0px;
// padding-left: 5px;
// padding-right: 5px;
// padding-bottom: 5px;
min-width: 10rem;
max-width: 10vw;
max-height: 4rem;
text-align: center;
justify-content: center;
font-family: 'Cute Font';
&.marked {
// background-color: brighten($village_color, 100%);
filter: hue-rotate(90deg);
}
&.connected {}
&.disconnected {
// background-color: $disconnected_color;
// border: 3px solid darken($disconnected_color, 20%);
}
&.dead {
filter: grayscale(100%);
}
.number {
padding-top: 3px; padding-top: 3px;
margin: 0px; margin: 0px;
&.not-set {
border: 2px solid rgba(255, 0, 0, 0.3);
background-color: rgba(255, 0, 0, 0.7);
}
}
} }
.player:hover { .submenu {
filter: brightness(120%); background-color: black;
border: 1px solid rgba(255, 255, 255, 0.7);
padding: 10px;
// position: absolute;
position: relative;
// top: 1px;
align-self: stretch;
z-index: 5;
& button {
width: 100%;
}
}
.click-backdrop {
z-index: 4;
background-color: rgba(0, 0, 0, 0.7);
position: fixed;
top: 0;
left: 0;
height: 200vh;
width: 100vw;
background-size: cover;
} }
.player-container { .player-container {
@ -137,26 +180,20 @@ button {
border: none; border: none;
width: fit-content;
height: fit-content;
outline: inherit; outline: inherit;
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
} background-color: #000;
button:disabled { &:disabled {
filter: grayscale(80%); filter: grayscale(80%);
} }
button:hover { &:disabled:hover {
filter: brightness(80%);
}
button:disabled:hover {
filter: sepia(100%); filter: sepia(100%);
} }
button:disabled:hover::after { &:disabled:hover::after {
content: attr(reason); content: attr(reason);
position: absolute; position: absolute;
margin-top: 10px; margin-top: 10px;
@ -170,8 +207,10 @@ button:disabled:hover::after {
min-width: 50vw; min-width: 50vw;
width: fit-content; width: fit-content;
padding: 3px; padding: 3px;
z-index: 3; z-index: 4;
} }
}
.settings { .settings {
list-style: none; list-style: none;
@ -396,7 +435,6 @@ client {
font-family: 'Cute Font'; font-family: 'Cute Font';
// font-size: 0.7rem; // font-size: 0.7rem;
background-color: hsl(280, 55%, 61%);
display: flex; display: flex;
// flex-wrap: wrap; // flex-wrap: wrap;
flex-direction: column; flex-direction: column;
@ -405,7 +443,6 @@ client {
padding: 30px; padding: 30px;
gap: 30px; gap: 30px;
filter: $client_filter;
border: 2px solid black; border: 2px solid black;
} }
@ -414,17 +451,10 @@ clients {
font-family: 'Cute Font'; font-family: 'Cute Font';
// font-size: 0.7rem; // font-size: 0.7rem;
background-color: white;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
flex-direction: row; flex-direction: row;
font-size: 2rem; font-size: 2rem;
margin-left: 20px;
margin-right: 20px;
gap: 10px;
border: solid 3px;
border-color: #432054;
} }
.role-reveal-cards { .role-reveal-cards {
@ -550,6 +580,19 @@ clients {
font-size: 1.2rem; font-size: 1.2rem;
} }
.client-nav {
// position: absolute;
// left: 0;
// top: 0;
width: 100%;
padding: 10px;
// background-color: rgba(255, 107, 255, 0.2);
display: flex;
flex-direction: row;
justify-content: baseline;
gap: 10px;
}
.ident { .ident {
gap: 0px; gap: 0px;
margin: 0px; margin: 0px;
@ -558,10 +601,11 @@ clients {
.submenu { .submenu {
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
display: flex; // display: flex;
justify-content: center; justify-content: center;
// visibility: collapse; // visibility: collapse;
display: none; display: none;
z-index: 5;
.button-container { .button-container {
display: flex; display: flex;
@ -571,6 +615,9 @@ clients {
&.shown { &.shown {
// visibility: visible; // visibility: visible;
display: flex; display: flex;
flex-direction: row;
align-items: baseline;
// position: absolute;
} }
button { button {
@ -617,40 +664,14 @@ error {
} }
} }
.player-list {
padding-bottom: 80px;
display: flex;
.player { flex-direction: row;
flex-wrap: wrap;
margin: 0px; gap: 10px;
padding-left: 5px; // align-items: center;
padding-right: 5px; justify-content: space-evenly;
text-align: center;
justify-content: center;
font-family: 'Cute Font';
background-color: $village_color;
border: 3px solid darken($village_color, 20%);
&.marked {
// background-color: brighten($village_color, 100%);
filter: hue-rotate(90deg);
}
&.connected {
background-color: $connected_color;
border: 3px solid darken($connected_color, 20%);
}
&.disconnected {
background-color: $disconnected_color;
border: 3px solid darken($disconnected_color, 20%);
}
&.dead {
filter: grayscale(100%);
}
} }
.binary { .binary {
@ -674,3 +695,45 @@ error {
} }
} }
} }
input {
background-color: rgba(255, 255, 255, 0.1);
color: white;
border: 2px solid rgba(255, 255, 255, 0.2);
margin: 10px;
}
.signin {
@extend .row-list;
justify-content: center;
text-align: center;
& label {
font-size: 1.5rem;
}
& input {
height: 2rem;
text-align: center;
&#number {
font-size: 2rem;
}
}
}
.info-update {
font-size: 2rem;
align-content: stretch;
margin: 0;
& * {
margin: 0;
width: 100%;
text-align: center;
}
}
.zoom {
zoom: 200%;
}

View File

@ -1,4 +1,4 @@
use core::{sync::atomic::AtomicBool, time::Duration}; use core::{num::NonZeroU8, sync::atomic::AtomicBool, time::Duration};
use std::collections::VecDeque; use std::collections::VecDeque;
use futures::{ use futures::{
@ -16,7 +16,6 @@ use werewolves_proto::{
game::GameOver, game::GameOver,
message::{ message::{
ClientMessage, DayCharacter, Identification, PlayerUpdate, PublicIdentity, ServerMessage, ClientMessage, DayCharacter, Identification, PlayerUpdate, PublicIdentity, ServerMessage,
Target,
night::{ActionPrompt, ActionResponse, ActionResult}, night::{ActionPrompt, ActionResponse, ActionResult},
}, },
player::PlayerId, player::PlayerId,
@ -25,7 +24,10 @@ use werewolves_proto::{
use yew::{html::Scope, prelude::*}; use yew::{html::Scope, prelude::*};
use crate::{ use crate::{
components::{InputName, Notification}, components::{
Button, Identity, Notification,
client::{ClientNav, InputName},
},
storage::StorageKey, storage::StorageKey,
}; };
@ -50,8 +52,8 @@ fn url() -> String {
format!( format!(
"{}client", "{}client",
option_env!("LOCAL") option_env!("LOCAL")
.map(|_| super::DEBUG_URL) .map(|_| crate::clients::DEBUG_URL)
.unwrap_or(super::LIVE_URL) .unwrap_or(crate::clients::LIVE_URL)
) )
} }
@ -185,7 +187,6 @@ impl Connection {
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum ClientEvent { pub enum ClientEvent {
Disconnected, Disconnected,
Notification(String),
Waiting, Waiting,
ShowRole(RoleTitle), ShowRole(RoleTitle),
NotInLobby(Box<[PublicIdentity]>), NotInLobby(Box<[PublicIdentity]>),
@ -201,13 +202,17 @@ impl TryFrom<ServerMessage> for ClientEvent {
Ok(match msg { Ok(match msg {
ServerMessage::Disconnect => Self::Disconnected, ServerMessage::Disconnect => Self::Disconnected,
ServerMessage::LobbyInfo { ServerMessage::LobbyInfo {
joined: false, joined,
players, mut players,
} => Self::NotInLobby(players), } => {
ServerMessage::LobbyInfo { const LAST: NonZeroU8 = NonZeroU8::new(0xFF).unwrap();
joined: true, players.sort_by(|l, r| l.number.unwrap_or(LAST).cmp(&r.number.unwrap_or(LAST)));
players, let players = players.into_iter().collect();
} => Self::InLobby(players), match joined {
true => Self::InLobby(players),
false => Self::NotInLobby(players),
}
}
ServerMessage::GameInProgress => Self::GameInProgress, ServerMessage::GameInProgress => Self::GameInProgress,
_ => return Err(msg), _ => return Err(msg),
}) })
@ -251,10 +256,9 @@ impl Component for Client {
fn create(ctx: &Context<Self>) -> Self { fn create(ctx: &Context<Self>) -> Self {
gloo::utils::document().set_title("Werewolves Player"); gloo::utils::document().set_title("Werewolves Player");
let player = StorageKey::PlayerId let player = PlayerId::load_from_storage()
.get()
.ok() .ok()
.and_then(|p| StorageKey::PublicIdentity.get().ok().map(|n| (p, n))) .and_then(|p| PublicIdentity::load_from_storage().ok().map(|n| (p, n)))
.map(|(player_id, public)| Identification { player_id, public }); .map(|(player_id, public)| Identification { player_id, public });
let (send, recv) = futures::channel::mpsc::channel::<ClientMessage>(100); let (send, recv) = futures::channel::mpsc::channel::<ClientMessage>(100);
@ -337,11 +341,11 @@ impl Component for Client {
); );
html! { html! {
<div class="lobby"> <div class="lobby">
<Button on_click={on_click}>{"Join"}</Button>
<p>{format!("Players in lobby: {}", players.len())}</p> <p>{format!("Players in lobby: {}", players.len())}</p>
<ul class="players"> <ul class="players">
{players.iter().map(|p| html!{<p>{p.to_string()}</p>}).collect::<Html>()} {players.iter().map(|p| html!{<p>{p.to_string()}</p>}).collect::<Html>()}
</ul> </ul>
<button onclick={on_click}>{"Join"}</button>
</div> </div>
} }
} }
@ -355,11 +359,11 @@ impl Component for Client {
); );
html! { html! {
<div class="lobby"> <div class="lobby">
<Button on_click={on_click}>{"Leave"}</Button>
<p>{format!("Players in lobby: {}", players.len())}</p> <p>{format!("Players in lobby: {}", players.len())}</p>
<ul class="players"> <ul class="players">
{players.iter().map(|p| html!{<p>{p.to_string()}</p>}).collect::<Html>()} {players.iter().map(|p| html!{<p>{p.to_string()}</p>}).collect::<Html>()}
</ul> </ul>
<button onclick={on_click}>{"Leave"}</button>
</div> </div>
} }
} }
@ -378,43 +382,42 @@ impl Component for Client {
}}</p> }}</p>
</div> </div>
}, },
ClientEvent::Notification(notification) => {
let scope = ctx.link().clone();
let next_event =
// Callback::from(move |_| scope.clone().send_message(Message::));
Callback::from(move |_| log::info!("nothing"));
html! {
<Notification text={notification.clone()} callback={next_event}/>
}
}
}; };
let player = self let player = self
.player .player
.as_ref() .as_ref()
.map(|player| { .map(|player| {
let pronouns = if let Some(pronouns) = player.public.pronouns.as_ref() {
html! { html! {
<p class={"pronouns"}>{"("}{pronouns.as_str()}{")"}</p> <Identity ident={player.public.clone()} class="zoom">
} </Identity>
} else {
html!()
};
html! {
<player>
<p>{player.public.number.get()}</p>
<name>{player.public.name.clone()}</name>
{pronouns}
</player>
} }
}) })
.unwrap_or(html!()); .unwrap_or(html!());
let send = self.send.clone();
let client_nav_msg_cb = move |msg| {
let mut send = send.clone();
yew::platform::spawn_local(async move {
if let Err(err) = send.send(msg).await {
log::error!("sending nav message: {err}");
}
});
};
let nav = self.player.as_ref().map(|_| {
html! { html! {
<ClientNav message_callback={client_nav_msg_cb} />
}
});
html! {
<>
{nav}
<client> <client>
{player} {player}
{content} {content}
</client> </client>
</>
} }
} }
@ -431,34 +434,45 @@ impl Component for Client {
} }
Message::SetPublicIdentity(public) => { Message::SetPublicIdentity(public) => {
match self.player.as_mut() { match self.player.as_mut() {
Some(p) => p.public = public, Some(p) => {
if let Err(err) = public.save_to_storage() {
self.error(err.into());
return false;
}
p.public = public;
}
None => { None => {
let res = let player_id = match PlayerId::load_from_storage() {
StorageKey::PlayerId Ok(pid) => pid,
.get_or_set(PlayerId::new) Err(StorageError::KeyNotFound(_)) => {
.and_then(|player_id| { let pid = PlayerId::new();
StorageKey::PublicIdentity if let Err(err) = pid.save_to_storage() {
.set(public.clone()) self.error(err.into());
.map(|_| Identification { player_id, public }) return false;
});
match res {
Ok(ident) => {
self.player = Some(ident.clone());
if let Some(recv) = self.recv.take() {
yew::platform::spawn_local(
Connection {
scope: ctx.link().clone(),
ident,
recv,
}
.run(),
);
} }
pid
} }
Err(err) => { Err(err) => {
self.error(err.into()); self.error(err.into());
return false; return false;
} }
};
if let Err(err) = public.save_to_storage() {
self.error(err.into());
return false;
}
let ident = Identification { player_id, public };
self.player = Some(ident.clone());
if let Some(recv) = self.recv.take() {
yew::platform::spawn_local(
Connection {
recv,
ident,
scope: ctx.link().clone(),
}
.run(),
);
} }
} }
} }
@ -469,8 +483,8 @@ impl Component for Client {
joined: false, joined: false,
players: _, players: _,
} = &msg } = &msg
&& self.auto_join
{ {
if self.auto_join {
let mut send = self.send.clone(); let mut send = self.send.clone();
yew::platform::spawn_local(async move { yew::platform::spawn_local(async move {
if let Err(err) = send.send(ClientMessage::Hello).await { if let Err(err) = send.send(ClientMessage::Hello).await {
@ -480,7 +494,6 @@ impl Component for Client {
self.auto_join = false; self.auto_join = false;
return false; return false;
} }
}
let msg = match msg.try_into() { let msg = match msg.try_into() {
Ok(event) => { Ok(event) => {
self.current_event.replace(event); self.current_event.replace(event);
@ -519,7 +532,7 @@ impl Component for Client {
ServerMessage::Sleep => self.current_event = Some(ClientEvent::Waiting), ServerMessage::Sleep => self.current_event = Some(ClientEvent::Waiting),
ServerMessage::Update(update) => match (update, self.player.as_mut()) { ServerMessage::Update(update) => match (update, self.player.as_mut()) {
(PlayerUpdate::Number(num), Some(player)) => { (PlayerUpdate::Number(num), Some(player)) => {
player.public.number = num; player.public.number = Some(num);
return true; return true;
} }
(_, None) => return false, (_, None) => return false,

View File

@ -1,4 +1,5 @@
use core::{num::NonZeroU8, ops::Not, time::Duration}; use core::{num::NonZeroU8, ops::Not, time::Duration};
use std::{rc::Rc, sync::Arc};
use futures::{ use futures::{
SinkExt, StreamExt, SinkExt, StreamExt,
@ -11,7 +12,7 @@ use werewolves_proto::{
error::GameError, error::GameError,
game::{GameOver, GameSettings}, game::{GameOver, GameSettings},
message::{ message::{
CharacterState, PlayerState, PublicIdentity, Target, CharacterIdentity, CharacterState, PlayerState, PublicIdentity,
host::{ host::{
HostDayMessage, HostGameMessage, HostLobbyMessage, HostMessage, HostNightMessage, HostDayMessage, HostGameMessage, HostLobbyMessage, HostMessage, HostNightMessage,
ServerToHostMessage, ServerToHostMessage,
@ -37,8 +38,8 @@ fn url() -> String {
format!( format!(
"{}host", "{}host",
option_env!("LOCAL") option_env!("LOCAL")
.map(|_| super::DEBUG_URL) .map(|_| crate::clients::DEBUG_URL)
.unwrap_or(super::LIVE_URL) .unwrap_or(crate::clients::LIVE_URL)
) )
} }
@ -184,7 +185,7 @@ pub enum HostEvent {
pub enum HostState { pub enum HostState {
Disconnected, Disconnected,
Lobby { Lobby {
players: Box<[PlayerState]>, players: Rc<[PlayerState]>,
settings: GameSettings, settings: GameSettings,
}, },
Day { Day {
@ -196,11 +197,11 @@ pub enum HostState {
result: GameOver, result: GameOver,
}, },
RoleReveal { RoleReveal {
ackd: Box<[Target]>, ackd: Box<[CharacterIdentity]>,
waiting: Box<[Target]>, waiting: Box<[CharacterIdentity]>,
}, },
Prompt(ActionPrompt), Prompt(ActionPrompt),
Result(Option<PublicIdentity>, ActionResult), Result(Option<CharacterIdentity>, ActionResult),
} }
impl From<ServerToHostMessage> for HostEvent { impl From<ServerToHostMessage> for HostEvent {
@ -340,7 +341,7 @@ impl Component for Host {
settings={settings} settings={settings}
on_start={on_start} on_start={on_start}
on_update={on_changed} on_update={on_changed}
players_in_lobby={players.len()} players_in_lobby={players.clone()}
/> />
} }
}); });
@ -352,6 +353,9 @@ impl Component for Host {
LobbyPlayerAction::Kick => { LobbyPlayerAction::Kick => {
HostMessage::Lobby(HostLobbyMessage::Kick(player_id)) HostMessage::Lobby(HostLobbyMessage::Kick(player_id))
} }
LobbyPlayerAction::SetNumber(num) => HostMessage::Lobby(
HostLobbyMessage::SetPlayerNumber(player_id, num),
),
}; };
let mut send = send.clone(); let mut send = send.clone();
let on_error = on_error.clone(); let on_error = on_error.clone();
@ -404,7 +408,7 @@ impl Component for Host {
HostState::RoleReveal { ackd, waiting } => { HostState::RoleReveal { ackd, waiting } => {
let send = self.send.clone(); let send = self.send.clone();
let on_force_ready = self.big_screen.not().then(|| { let on_force_ready = self.big_screen.not().then(|| {
Callback::from(move |target: Target| { Callback::from(move |target: CharacterIdentity| {
let send = send.clone(); let send = send.clone();
yew::platform::spawn_local(async move { yew::platform::spawn_local(async move {
if let Err(err) = send if let Err(err) = send
@ -456,7 +460,7 @@ impl Component for Host {
result={result} result={result}
big_screen={self.big_screen} big_screen={self.big_screen}
on_complete={on_complete} on_complete={on_complete}
ident={ident} ident={ident.map(|i| i.into())}
/> />
} }
} }
@ -471,7 +475,7 @@ impl Component for Host {
self.send.clone(), self.send.clone(),
); );
html! { html! {
<nav class="debug-nav" style="z-index: 10;"> <nav class="debug-nav" style="z-index: 3;">
<Button on_click={on_error_click}>{"error"}</Button> <Button on_click={on_error_click}>{"error"}</Button>
<Button on_click={on_prev_click}>{"previous"}</Button> <Button on_click={on_prev_click}>{"previous"}</Button>
</nav> </nav>
@ -489,17 +493,26 @@ impl Component for Host {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg { match msg {
HostEvent::PlayerList(players) => { HostEvent::PlayerList(mut players) => {
const LAST: NonZeroU8 = NonZeroU8::new(0xFF).unwrap();
players.sort_by(|l, r| {
l.identification
.public
.number
.unwrap_or(LAST)
.cmp(&r.identification.public.number.unwrap_or(LAST))
});
match &mut self.state { match &mut self.state {
HostState::Lobby { HostState::Lobby {
players: p, players: p,
settings: _, settings: _,
} => *p = players, } => *p = players.into_iter().collect(),
HostState::Disconnected | HostState::GameOver { result: _ } => { HostState::Disconnected | HostState::GameOver { result: _ } => {
let mut send = self.send.clone(); let mut send = self.send.clone();
let on_err = self.error_callback.clone(); let on_err = self.error_callback.clone();
self.state = HostState::Lobby { self.state = HostState::Lobby {
players, players: players.into_iter().collect(),
settings: Default::default(), settings: Default::default(),
}; };
yew::platform::spawn_local(async move { yew::platform::spawn_local(async move {

View File

@ -1,8 +1,10 @@
pub mod client { pub mod client {
include!("client/client.rs"); mod client;
pub use client::*;
} }
pub mod host { pub mod host {
include!("host/host.rs"); mod host;
pub use host::*;
} }
// mod socket; // mod socket;

View File

@ -2,7 +2,7 @@ use core::ops::Not;
use werewolves_proto::{ use werewolves_proto::{
message::{ message::{
PublicIdentity, CharacterIdentity, PublicIdentity,
host::{HostGameMessage, HostMessage, HostNightMessage}, host::{HostGameMessage, HostMessage, HostNightMessage},
night::{ActionPrompt, ActionResponse}, night::{ActionPrompt, ActionResponse},
}, },
@ -24,14 +24,15 @@ pub struct ActionPromptProps {
pub on_complete: Callback<HostMessage>, pub on_complete: Callback<HostMessage>,
} }
fn identity_html(props: &ActionPromptProps, ident: Option<&PublicIdentity>) -> Option<Html> { fn identity_html(props: &ActionPromptProps, ident: Option<&CharacterIdentity>) -> Option<Html> {
props props
.big_screen .big_screen
.not() .not()
.then(|| { .then(|| {
ident.map(|ident| { ident.map(|ident| {
let ident: PublicIdentity = ident.into();
html! { html! {
<Identity ident={ident.clone()}/> <Identity ident={ident}/>
} }
}) })
}) })
@ -85,7 +86,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{identity_html(props, Some(&character_id.public))} {identity_html(props, Some(&character_id))}
<SingleTarget <SingleTarget
targets={living_players.clone()} targets={living_players.clone()}
target_selection={on_select} target_selection={on_select}
@ -111,7 +112,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{identity_html(props, Some(&character_id.public))} {identity_html(props, Some(&character_id))}
<h2>{"your role has changed"}</h2> <h2>{"your role has changed"}</h2>
<p>{new_role.to_string()}</p> <p>{new_role.to_string()}</p>
{cont} {cont}
@ -132,7 +133,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{identity_html(props, Some(&character_id.public))} {identity_html(props, Some(&character_id))}
<SingleTarget <SingleTarget
targets={targets.clone()} targets={targets.clone()}
target_selection={on_select} target_selection={on_select}
@ -155,7 +156,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{identity_html(props, Some(&character_id.public))} {identity_html(props, Some(&character_id))}
<TwoTarget <TwoTarget
targets={living_players.clone()} targets={living_players.clone()}
target_selection={on_select} target_selection={on_select}
@ -178,7 +179,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{identity_html(props, Some(&character_id.public))} {identity_html(props, Some(&character_id))}
<SingleTarget <SingleTarget
targets={dead_players.clone()} targets={dead_players.clone()}
target_selection={on_select} target_selection={on_select}
@ -202,7 +203,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{identity_html(props, Some(&character_id.public))} {identity_html(props, Some(&character_id))}
<SingleTarget <SingleTarget
targets={living_players.clone()} targets={living_players.clone()}
target_selection={on_select} target_selection={on_select}
@ -210,7 +211,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
> >
<h3> <h3>
<b>{"current target: "}</b>{current_target.clone().map(|t| html!{ <b>{"current target: "}</b>{current_target.clone().map(|t| html!{
<Identity ident={t.public} /> <Identity ident={Into::<PublicIdentity>::into(t)} />
}).unwrap_or_else(|| html!{<i>{"none"}</i>})} }).unwrap_or_else(|| html!{<i>{"none"}</i>})}
</h3> </h3>
</SingleTarget> </SingleTarget>
@ -231,7 +232,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{identity_html(props, Some(&character_id.public))} {identity_html(props, Some(&character_id))}
<OptionalSingleTarget <OptionalSingleTarget
targets={living_players.clone()} targets={living_players.clone()}
target_selection={on_select} target_selection={on_select}
@ -260,7 +261,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{identity_html(props, Some(&character_id.public))} {identity_html(props, Some(&character_id))}
<OptionalSingleTarget <OptionalSingleTarget
targets={living_players.clone()} targets={living_players.clone()}
target_selection={on_select} target_selection={on_select}
@ -281,14 +282,14 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
html! { html! {
<> <>
<b>{"last night you protected: "}</b> <b>{"last night you protected: "}</b>
<Identity ident={target.public.clone()}/> <Identity ident={Into::<PublicIdentity>::into(target)}/>
</> </>
} }
} }
PreviousGuardianAction::Guard(target) => html! { PreviousGuardianAction::Guard(target) => html! {
<> <>
<b>{"last night you guarded: "}</b> <b>{"last night you guarded: "}</b>
<Identity ident={target.public.clone()}/> <Identity ident={Into::<PublicIdentity>::into(target)}/>
</> </>
}, },
}); });
@ -303,7 +304,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
html! { html! {
<div> <div>
{identity_html(props, Some(&character_id.public))} {identity_html(props, Some(&character_id))}
<SingleTarget <SingleTarget
targets={living_players.clone()} targets={living_players.clone()}
target_selection={on_select} target_selection={on_select}
@ -342,7 +343,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{identity_html(props, Some(&character_id.public))} {identity_html(props, Some(&character_id))}
<BinaryChoice on_chosen={on_select}> <BinaryChoice on_chosen={on_select}>
<h2>{"shapeshift?"}</h2> <h2>{"shapeshift?"}</h2>
</BinaryChoice> </BinaryChoice>
@ -363,7 +364,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{identity_html(props, Some(&character_id.public))} {identity_html(props, Some(&character_id))}
<OptionalSingleTarget <OptionalSingleTarget
targets={living_villagers.clone()} targets={living_villagers.clone()}
target_selection={on_select} target_selection={on_select}
@ -386,7 +387,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}); });
html! { html! {
<div> <div>
{identity_html(props, Some(&character_id.public))} {identity_html(props, Some(&character_id))}
<SingleTarget <SingleTarget
targets={living_players.clone()} targets={living_players.clone()}
target_selection={on_select} target_selection={on_select}

View File

@ -2,14 +2,17 @@ use core::{fmt::Debug, ops::Not};
use std::sync::Arc; use std::sync::Arc;
use werewolves_macros::ChecksAs; use werewolves_macros::ChecksAs;
use werewolves_proto::{message::Target, player::CharacterId}; use werewolves_proto::{
message::{CharacterIdentity, PublicIdentity},
player::CharacterId,
};
use yew::prelude::*; use yew::prelude::*;
use crate::components::{Button, Identity}; use crate::components::{Button, Identity};
#[derive(Debug, Clone, PartialEq, Properties)] #[derive(Debug, Clone, PartialEq, Properties)]
pub struct TwoTargetProps { pub struct TwoTargetProps {
pub targets: Box<[Target]>, pub targets: Box<[CharacterIdentity]>,
#[prop_or_default] #[prop_or_default]
pub headline: &'static str, pub headline: &'static str,
#[prop_or_default] #[prop_or_default]
@ -52,7 +55,7 @@ impl Component for TwoTarget {
target_selection, target_selection,
} = ctx.props(); } = ctx.props();
let mut targets = targets.clone(); let mut targets = targets.clone();
targets.sort_by(|l, r| l.public.number.cmp(&r.public.number)); targets.sort_by(|l, r| l.number.cmp(&r.number));
let target_selection = target_selection.clone(); let target_selection = target_selection.clone();
let scope = ctx.link().clone(); let scope = ctx.link().clone();
@ -136,7 +139,7 @@ impl Component for TwoTarget {
#[derive(Debug, Clone, PartialEq, Properties)] #[derive(Debug, Clone, PartialEq, Properties)]
pub struct OptionalSingleTargetProps { pub struct OptionalSingleTargetProps {
pub targets: Box<[Target]>, pub targets: Box<[CharacterIdentity]>,
#[prop_or_default] #[prop_or_default]
pub headline: &'static str, pub headline: &'static str,
#[prop_or_default] #[prop_or_default]
@ -164,7 +167,7 @@ impl Component for OptionalSingleTarget {
children, children,
} = ctx.props(); } = ctx.props();
let mut targets = targets.clone(); let mut targets = targets.clone();
targets.sort_by(|l, r| l.public.number.cmp(&r.public.number)); targets.sort_by(|l, r| l.number.cmp(&r.number));
let target_selection = target_selection.clone(); let target_selection = target_selection.clone();
let scope = ctx.link().clone(); let scope = ctx.link().clone();
@ -233,7 +236,7 @@ impl Component for OptionalSingleTarget {
#[derive(Debug, Clone, PartialEq, Properties)] #[derive(Debug, Clone, PartialEq, Properties)]
pub struct SingleTargetProps { pub struct SingleTargetProps {
pub targets: Box<[Target]>, pub targets: Box<[CharacterIdentity]>,
#[prop_or_default] #[prop_or_default]
pub headline: &'static str, pub headline: &'static str,
#[prop_or_default] #[prop_or_default]
@ -263,7 +266,7 @@ impl Component for SingleTarget {
children, children,
} = ctx.props(); } = ctx.props();
let mut targets = targets.clone(); let mut targets = targets.clone();
targets.sort_by(|l, r| l.public.number.cmp(&r.public.number)); targets.sort_by(|l, r| l.number.cmp(&r.number));
let target_selection = target_selection.clone(); let target_selection = target_selection.clone();
let scope = ctx.link().clone(); let scope = ctx.link().clone();
let card_select = Callback::from(move |target| { let card_select = Callback::from(move |target| {
@ -335,7 +338,7 @@ impl Component for SingleTarget {
#[derive(Debug, Clone, PartialEq, Properties)] #[derive(Debug, Clone, PartialEq, Properties)]
pub struct TargetCardProps { pub struct TargetCardProps {
pub target: Target, pub target: CharacterIdentity,
pub selected: bool, pub selected: bool,
pub on_select: Callback<CharacterId>, pub on_select: Callback<CharacterId>,
} }
@ -357,7 +360,7 @@ fn TargetCard(props: &TargetCardProps) -> Html {
html! { html! {
<div class={"row-list baseline margin-5"}> <div class={"row-list baseline margin-5"}>
<div class={classes!("player", "ident", "column-list", marked)}> <div class={classes!("player", "ident", "column-list", marked)}>
<Identity ident={props.target.public.clone()} /> <Identity ident={Into::<PublicIdentity>::into(&props.target)} />
{submenu} {submenu}
</div> </div>
</div> </div>
@ -366,7 +369,7 @@ fn TargetCard(props: &TargetCardProps) -> Html {
#[derive(Debug, Clone, PartialEq, Properties)] #[derive(Debug, Clone, PartialEq, Properties)]
pub struct CustomTargetCardProps { pub struct CustomTargetCardProps {
pub target: Target, pub target: CharacterIdentity,
pub options: Arc<[String]>, pub options: Arc<[String]>,
pub on_select: Option<Callback<(CharacterId, String)>>, pub on_select: Option<Callback<(CharacterId, String)>>,
#[prop_or_default] #[prop_or_default]
@ -414,7 +417,7 @@ pub fn CustomTargetCard(
html! { html! {
<div class={"row-list baseline"}> <div class={"row-list baseline"}>
<div class={classes!("ident", "column-list", class)}> <div class={classes!("ident", "column-list", class)}>
<Identity ident={target.public.clone()} /> <Identity ident={Into::<PublicIdentity>::into(target)} />
{submenu} {submenu}
</div> </div>
</div> </div>

View File

@ -1,13 +1,16 @@
use core::ops::Not; use core::ops::Not;
use werewolves_proto::{message::Target, role::RoleTitle}; use werewolves_proto::{
message::{CharacterIdentity, PublicIdentity},
role::RoleTitle,
};
use yew::prelude::*; use yew::prelude::*;
use crate::components::{Button, Identity}; use crate::components::{Button, Identity};
#[derive(Debug, Clone, PartialEq, Properties)] #[derive(Debug, Clone, PartialEq, Properties)]
pub struct WolvesIntroProps { pub struct WolvesIntroProps {
pub wolves: Box<[(Target, RoleTitle)]>, pub wolves: Box<[(CharacterIdentity, RoleTitle)]>,
pub big_screen: bool, pub big_screen: bool,
pub on_complete: Callback<()>, pub on_complete: Callback<()>,
} }
@ -24,7 +27,7 @@ pub fn WolvesIntro(props: &WolvesIntroProps) -> Html {
props.wolves.iter().map(|w| html!{ props.wolves.iter().map(|w| html!{
<div class="character wolves"> <div class="character wolves">
<p class="role">{w.1.to_string()}</p> <p class="role">{w.1.to_string()}</p>
<Identity ident={w.0.public.clone()} /> <Identity ident={Into::<PublicIdentity>::into(&w.0)} />
</div> </div>
}).collect::<Html>() }).collect::<Html>()
} }

View File

@ -14,7 +14,6 @@ pub fn Button(props: &ButtonProperties) -> Html {
let on_click = props.on_click.clone(); let on_click = props.on_click.clone();
let on_click = Callback::from(move |_| on_click.emit(())); let on_click = Callback::from(move |_| on_click.emit(()));
html! { html! {
<div class="button-container">
<button <button
class="default-button" class="default-button"
disabled={props.disabled_reason.is_some()} disabled={props.disabled_reason.is_some()}
@ -23,6 +22,5 @@ pub fn Button(props: &ButtonProperties) -> Html {
> >
{props.children.clone()} {props.children.clone()}
</button> </button>
</div>
} }
} }

View File

@ -0,0 +1,207 @@
use core::{fmt::Debug, num::NonZeroU8, ops::Not};
use werewolves_proto::{
message::{ClientMessage, PublicIdentity, UpdateSelf},
player::PlayerId,
};
use yew::prelude::*;
use crate::{
components::{Button, ClickableField, ClickableNumberEdit},
storage::StorageKey,
};
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct ClientNavProps {
pub message_callback: Callback<ClientMessage>,
}
#[function_component]
pub fn ClientNav(ClientNavProps { message_callback }: &ClientNavProps) -> Html {
let ident = use_state(|| {
PublicIdentity::load_from_storage().expect("don't call client nav without an identity")
});
let pronouns = ident
.pronouns
.as_ref()
.map(|pronouns| {
html! {
<div>{"("}{pronouns.as_str()}{")"}</div>
}
})
.unwrap_or_else(|| {
html! {
<div>{"(None)"}</div>
}
});
let number = {
let current_value = use_state(String::new);
let message_callback = message_callback.clone();
let submit_ident = ident.clone();
let current_num = ident
.number
.map(|v| v.to_string())
.unwrap_or_else(|| String::from("???"));
let open = use_state(|| false);
let open_set = open.setter();
let on_submit = {
let val = current_value.clone();
Callback::from(move |_| {
let num = match val.trim().parse::<NonZeroU8>().ok() {
Some(num) => num,
None => return,
};
message_callback.emit(ClientMessage::UpdateSelf(UpdateSelf::Number(num)));
let new_ident = PublicIdentity {
name: submit_ident.name.clone(),
pronouns: submit_ident.pronouns.clone(),
number: Some(num),
};
submit_ident.set(new_ident.clone());
if let Err(err) = new_ident.save_to_storage() {
log::error!("saving public identity after change: {err}");
}
open_set.set(false);
})
};
html! {
<ClickableNumberEdit
value={current_value.clone()}
field_name="number"
on_submit={on_submit}
state={open}
>
<div class="number">{current_num}</div>
</ClickableNumberEdit>
}
};
let name = {
let open = use_state(|| false);
let name = use_state(String::new);
let on_submit = {
let ident = ident.clone();
let message_callback = message_callback.clone();
Callback::from(move |value: String| -> Option<PublicIdentity> {
value.trim().is_empty().not().then(|| {
let name = value.trim().to_string();
message_callback
.emit(ClientMessage::UpdateSelf(UpdateSelf::Name(name.clone())));
PublicIdentity {
name,
number: ident.number,
pronouns: ident.pronouns.clone(),
}
})
})
};
html! {
<ClickableTextEdit
value={name.clone()}
submit_ident={ident.clone()}
field_name="pronouns"
on_submit={on_submit}
state={open}
>
<div class="name">{ident.name.as_str()}</div>
</ClickableTextEdit>
}
};
let pronouns = {
let pronouns_state = use_state(String::new);
let on_submit = {
let ident = ident.clone();
let message_callback = message_callback.clone();
Callback::from(move |value: String| -> Option<PublicIdentity> {
let pronouns = value.trim().is_empty().not().then_some(value);
message_callback.emit(ClientMessage::UpdateSelf(UpdateSelf::Pronouns(
pronouns.clone(),
)));
Some(PublicIdentity {
pronouns,
name: ident.name.clone(),
number: ident.number,
})
})
};
let open = use_state(|| false);
html! {
<ClickableTextEdit
value={pronouns_state}
submit_ident={ident.clone()}
field_name="pronouns"
on_submit={on_submit}
state={open}
>
{pronouns}
</ClickableTextEdit>
}
};
let cb = message_callback.clone();
let forgor = move |_| {
PlayerId::delete();
PublicIdentity::delete();
cb.emit(ClientMessage::Goodbye);
let _ = gloo::utils::window().location().reload();
};
html! {
<nav class="client-nav">
{number}
{name}
{pronouns}
<Button on_click={forgor}>{"forgor 💀"}</Button>
</nav>
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
struct ClickableTextEditProps {
#[prop_or_default]
pub children: Html,
pub value: UseStateHandle<String>,
pub submit_ident: UseStateHandle<PublicIdentity>,
pub on_submit: Callback<String, Option<PublicIdentity>>,
pub field_name: &'static str,
pub state: UseStateHandle<bool>,
}
#[function_component]
fn ClickableTextEdit(
ClickableTextEditProps {
children,
value,
submit_ident,
field_name,
on_submit,
state,
}: &ClickableTextEditProps,
) -> Html {
let on_input = crate::components::input_element_string_oninput(value.setter());
let submit_ident = submit_ident.clone();
let message_callback = on_submit.clone();
let value = value.clone();
let open_set = state.setter();
let submit = move |_| {
if let Some(new_ident) = message_callback.emit(value.trim().to_string()) {
submit_ident.set(new_ident.clone());
if let Err(err) = new_ident.save_to_storage() {
log::error!("saving public identity after change: {err}");
}
open_set.set(false);
}
};
let options = html! {
<div class="row-list info-update">
<input type="text" oninput={on_input} name={*field_name}/>
<Button on_click={submit}>{"ok"}</Button>
</div>
};
html! {
<ClickableField options={options} state={state.clone()}>
{children.clone()}
</ClickableField>
}
}

View File

@ -0,0 +1,82 @@
use core::num::NonZeroU8;
use web_sys::HtmlInputElement;
use werewolves_proto::message::PublicIdentity;
use yew::prelude::*;
use crate::components::Button;
#[derive(Debug, PartialEq, Properties)]
pub struct InputProps {
pub callback: Callback<PublicIdentity, ()>,
}
#[function_component]
pub fn InputName(props: &InputProps) -> Html {
let callback = props.callback.clone();
let num_value = use_state(String::new);
let name_value = use_state(String::new);
let pronouns_value = use_state(String::new);
let on_input_update = |state: UseStateSetter<String>| {
move |ev: InputEvent| {
if let Some(target) = ev.target_dyn_into::<HtmlInputElement>() {
state.set(target.value());
}
}
};
let name_on_input = on_input_update(name_value.setter());
let pronouns_on_input = on_input_update(pronouns_value.setter());
let on_click_num_value = num_value.clone();
let on_click = Callback::from(move |_| {
let name = name_value.trim().to_string();
let pronouns = match pronouns_value.trim() {
"" => None,
p => Some(p.to_string()),
};
let number = on_click_num_value.parse::<NonZeroU8>().ok();
if name.is_empty() {
return;
}
callback.emit(PublicIdentity {
name,
pronouns,
number,
});
});
// let num_value = num_value.clone();
// let on_change = move |ev: InputEvent| {
// let data = ev.data();
// if let Some(z) = data.as_ref().and_then(|d| d.trim().parse::<u8>().ok())
// && !(z == 0 && num_value.is_empty())
// {
// let new_value = format!("{}{z}", num_value.as_str());
// num_value.set(new_value);
// return;
// } else if data.is_none()
// && let Some(target) = ev.target_dyn_into::<HtmlInputElement>()
// {
// num_value.set(target.value());
// return;
// }
// if let Some(target) = ev.target_dyn_into::<HtmlInputElement>() {
// target.set_value(num_value.as_str());
// }
// };
let on_change = crate::components::input_element_number_oninput(num_value);
html! {
<div class="signin">
<div class="column-list">
<label for="name">{"Name"}</label>
<input oninput={name_on_input} name="name" id="name" type="text"/>
<label for="pronouns">{"Pronouns"}</label>
<input oninput={pronouns_on_input} name="pronouns" id="pronouns" type="text"/>
<label for="number">{"Number"}</label>
<input oninput={on_change} type="text" name="number" id="number"/>
<Button on_click={on_click}>{"Submit"}</Button>
</div>
</div>
}
}

View File

@ -0,0 +1,89 @@
use yew::prelude::*;
use crate::components::Button;
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct ClickableFieldProps {
#[prop_or_default]
pub children: Html,
pub options: Html,
#[prop_or_default]
pub class: yew::Classes,
#[prop_or_default]
pub with_backdrop_exit: bool,
pub state: UseStateHandle<bool>,
}
#[function_component]
pub fn ClickableField(
ClickableFieldProps {
children,
options,
class,
with_backdrop_exit,
state,
}: &ClickableFieldProps,
) -> Html {
let open = state.clone();
let on_click_open = open.clone();
let open_close = Callback::from(move |_| on_click_open.set(!(*on_click_open)));
let submenu_open_close = open_close.clone();
let submenu = open.clone().then(|| {
let backdrop = with_backdrop_exit.then(|| {
html! {
<div onclick={move |_| submenu_open_close.emit(())} class="click-backdrop"/>
}
});
html! {
<>
<nav class="submenu shown">
{options.clone()}
</nav>
{backdrop}
</>
}
});
html! {
<div class={class.clone()}>
<Button on_click={open_close}>{children.clone()}</Button>
{submenu}
</div>
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct ClickableNumberEditProps {
#[prop_or_default]
pub children: Html,
pub value: UseStateHandle<String>,
pub on_submit: Callback<()>,
pub field_name: &'static str,
pub state: UseStateHandle<bool>,
}
#[function_component]
pub fn ClickableNumberEdit(
ClickableNumberEditProps {
children,
value,
field_name,
on_submit,
state,
}: &ClickableNumberEditProps,
) -> Html {
let on_input = crate::components::input_element_string_oninput(value.setter());
let on_submit = on_submit.clone();
let options = html! {
<div class="row-list info-update">
<input type="text" oninput={on_input} name={*field_name}/>
<Button on_click={on_submit.clone()}>{"ok"}</Button>
</div>
};
html! {
<ClickableField options={options} state={state.clone()}>
{children.clone()}
</ClickableField>
}
}

View File

@ -1,6 +1,9 @@
use core::ops::Not; use core::ops::Not;
use werewolves_proto::{message::CharacterState, player::CharacterId}; use werewolves_proto::{
message::{CharacterState, PublicIdentity},
player::CharacterId,
};
use yew::prelude::*; use yew::prelude::*;
use crate::components::{Button, Identity}; use crate::components::{Button, Identity};
@ -26,14 +29,14 @@ pub fn DaytimePlayerList(
) -> Html { ) -> Html {
let on_select = big_screen.not().then(|| on_mark.clone()); let on_select = big_screen.not().then(|| on_mark.clone());
let mut characters = characters.clone(); let mut characters = characters.clone();
characters.sort_by(|l, r| l.public_identity.number.cmp(&r.public_identity.number)); characters.sort_by(|l, r| l.identity.number.cmp(&r.identity.number));
let chars = characters let chars = characters
.iter() .iter()
.map(|c| { .map(|c| {
html! { html! {
<DaytimePlayer <DaytimePlayer
character={c.clone()} character={c.clone()}
on_the_block={marked.contains(&c.character_id)} on_the_block={marked.contains(&c.identity.character_id)}
on_select={on_select.clone()} on_select={on_select.clone()}
/> />
} }
@ -77,10 +80,9 @@ pub fn DaytimePlayer(
character: character:
CharacterState { CharacterState {
player_id: _, player_id: _,
character_id,
public_identity,
role: _, role: _,
died_to, died_to,
identity,
}, },
}: &DaytimePlayerProps, }: &DaytimePlayerProps,
) -> Html { ) -> Html {
@ -89,7 +91,7 @@ pub fn DaytimePlayer(
let on_the_block = on_the_block.then_some("marked"); let on_the_block = on_the_block.then_some("marked");
let submenu = died_to.is_none().then_some(()).and_then(|_| { let submenu = died_to.is_none().then_some(()).and_then(|_| {
on_select.as_ref().map(|on_select| { on_select.as_ref().map(|on_select| {
let character_id = character_id.clone(); let character_id = identity.character_id.clone();
let on_select = on_select.clone(); let on_select = on_select.clone();
let on_click = Callback::from(move |_| on_select.emit(character_id.clone())); let on_click = Callback::from(move |_| on_select.emit(character_id.clone()));
html! { html! {
@ -100,9 +102,10 @@ pub fn DaytimePlayer(
}) })
}); });
let identity: PublicIdentity = identity.into();
html! { html! {
<div class={classes!("player", dead, on_the_block, "column-list", "ident")}> <div class={classes!("player", dead, on_the_block, "column-list", "ident")}>
<Identity ident={public_identity.clone()}/> <Identity ident={identity}/>
{submenu} {submenu}
</div> </div>
} }

View File

@ -1,3 +1,4 @@
use core::ops::Deref;
use werewolves_proto::message::PublicIdentity; use werewolves_proto::message::PublicIdentity;
use yew::prelude::*; use yew::prelude::*;
@ -24,11 +25,29 @@ pub fn Identity(props: &IdentityProps) -> Html {
<p class="pronouns">{"("}{p}{")"}</p> <p class="pronouns">{"("}{p}{")"}</p>
} }
}); });
let not_set = number.is_none().then_some("not-set");
let number = number
.map(|n| n.to_string())
.unwrap_or_else(|| String::from("???"));
html! { html! {
<div class={classes!("identity", class)}> <div class={classes!("identity", class)}>
<p class="number"><b>{number.get()}</b></p> <p class={classes!("number", not_set)}><b>{number}</b></p>
<p>{name}</p> <p>{name}</p>
{pronouns} {pronouns}
</div> </div>
} }
} }
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct StatefulIdentityProps {
pub ident: UseStateHandle<PublicIdentity>,
#[prop_or_default]
pub class: Option<String>,
}
#[function_component]
pub fn StatefulIdentity(props: &StatefulIdentityProps) -> Html {
html! {
<Identity ident={props.ident.deref().clone()} class={props.class.clone()}/>
}
}

View File

@ -0,0 +1,29 @@
use core::num::NonZeroU8;
use web_sys::HtmlInputElement;
use yew::prelude::*;
pub fn input_element_number_oninput(num_value: UseStateHandle<String>) -> Callback<InputEvent> {
Callback::from(move |ev: InputEvent| {
let target = if let Some(target) = ev.target_dyn_into::<HtmlInputElement>() {
target
} else {
log::error!("cannot find target for input event");
return;
};
let value = target.value();
if let Ok(z) = value.trim().parse::<NonZeroU8>() {
num_value.set(z.to_string());
} else {
target.set_value(num_value.as_str());
}
})
}
pub fn input_element_string_oninput(value: UseStateSetter<String>) -> Callback<InputEvent> {
Callback::from(move |ev: InputEvent| {
if let Some(target) = ev.target_dyn_into::<HtmlInputElement>() {
value.set(target.value());
}
})
}

View File

@ -1,73 +0,0 @@
use core::num::NonZeroU8;
use web_sys::{HtmlInputElement, HtmlSelectElement, wasm_bindgen::JsCast};
use werewolves_proto::message::PublicIdentity;
use yew::prelude::*;
#[derive(Debug, PartialEq, Properties)]
pub struct InputProps {
#[prop_or_default]
pub initial_value: PublicIdentity,
pub callback: Callback<PublicIdentity, ()>,
}
#[function_component]
pub fn InputName(props: &InputProps) -> Html {
let callback = props.callback.clone();
let on_click = Callback::from(move |_| {
let name = gloo::utils::document()
.query_selector(".identity-input #name")
.expect("cannot find name input")
.and_then(|e| e.dyn_into::<HtmlInputElement>().ok())
.expect("name input element not HtmlInputElement")
.value()
.trim()
.to_string();
let pronouns = match gloo::utils::document()
.query_selector(".identity-input #pronouns")
.expect("cannot find pronouns input")
.and_then(|e| e.dyn_into::<HtmlInputElement>().ok())
.expect("pronouns input element not HtmlInputElement")
.value()
.trim()
{
"" => None,
p => Some(p.to_string()),
};
let number = gloo::utils::document()
.query_selector(".identity-input #number")
.expect("cannot find number input")
.and_then(|e| e.dyn_into::<HtmlSelectElement>().ok())
.expect("number input element not HtmlSelectElement")
.value()
.trim()
.parse::<NonZeroU8>()
.expect("parse number");
if name.is_empty() {
return;
}
callback.emit(PublicIdentity {
name,
pronouns,
number,
});
});
html! {
<div class="identity-input">
<label for="name">{"Name"}</label>
<input name="name" id="name" type="text" value={props.initial_value.name.clone()}/>
<label for="pronouns">{"Pronouns"}</label>
<input name="pronouns" id="pronouns" type="text"
value={props.initial_value.pronouns.clone().unwrap_or_default()}/>
<label for="number">{"Number"}</label>
<select name="number" id="number">
{
(1..=0xFFu8).into_iter().map(|i| html!{
<option value={i.to_string()}>{i}</option>
}).collect::<Html>()
}
</select>
<button onclick={on_click}>{"Submit"}</button>
</div>
}
}

View File

@ -1,3 +1,6 @@
use core::num::NonZeroU8;
use std::rc::Rc;
use werewolves_proto::{message::PlayerState, player::PlayerId}; use werewolves_proto::{message::PlayerState, player::PlayerId};
use yew::prelude::*; use yew::prelude::*;
@ -5,23 +8,21 @@ use crate::components::{LobbyPlayer, LobbyPlayerAction};
#[derive(Debug, PartialEq, Properties)] #[derive(Debug, PartialEq, Properties)]
pub struct LobbyProps { pub struct LobbyProps {
pub players: Box<[PlayerState]>, pub players: Rc<[PlayerState]>,
#[prop_or_default] #[prop_or_default]
pub on_action: Option<Callback<(PlayerId, LobbyPlayerAction)>>, pub on_action: Option<Callback<(PlayerId, LobbyPlayerAction)>>,
} }
#[function_component] #[function_component]
pub fn Lobby(LobbyProps { players, on_action }: &LobbyProps) -> Html { pub fn Lobby(LobbyProps { players, on_action }: &LobbyProps) -> Html {
let mut players = players.clone();
players.sort_by_key(|f| f.identification.public.number.get());
html! { html! {
<div class="column-list"> <div class="column-list">
<p style="text-align: center;">{format!("Players in lobby: {}", players.len())}</p> <p style="text-align: center;">{format!("Players in lobby: {}", players.len())}</p>
<div class="row-list small baseline player-list gap"> <div class="player-list">
{ {
players players
.into_iter() .into_iter()
.map(|p| html! {<LobbyPlayer on_action={on_action} player={p} />}) .map(|p| html! {<LobbyPlayer on_action={on_action} player={p.clone()} />})
.collect::<Html>() .collect::<Html>()
} }
</div> </div>

View File

@ -1,8 +1,10 @@
use core::num::NonZeroU8;
use web_sys::{HtmlDivElement, HtmlElement}; use web_sys::{HtmlDivElement, HtmlElement};
use werewolves_proto::{message::PlayerState, player::PlayerId}; use werewolves_proto::{message::PlayerState, player::PlayerId};
use yew::prelude::*; use yew::prelude::*;
use crate::components::{Button, Identity}; use crate::components::{Button, ClickableField, ClickableNumberEdit, Identity};
#[derive(Debug, PartialEq, Properties)] #[derive(Debug, PartialEq, Properties)]
pub struct LobbyPlayerProps { pub struct LobbyPlayerProps {
@ -14,6 +16,7 @@ pub struct LobbyPlayerProps {
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum LobbyPlayerAction { pub enum LobbyPlayerAction {
Kick, Kick,
SetNumber(NonZeroU8),
} }
#[function_component] #[function_component]
@ -26,25 +29,54 @@ pub fn LobbyPlayer(LobbyPlayerProps { player, on_action }: &LobbyPlayerProps) ->
let pid = player.identification.player_id.clone(); let pid = player.identification.player_id.clone();
let action = |action: LobbyPlayerAction| { let action = |action: LobbyPlayerAction| {
let pid = pid.clone(); let pid = pid.clone();
if let Some(on_action) = on_action.as_ref() { on_action
let on_action = on_action.clone(); .as_ref()
Callback::from(move |_| on_action.emit((pid.clone(), action))) .cloned()
} else { .map(|on_action| Callback::from(move |_| on_action.emit((pid.clone(), action))))
Callback::noop() .unwrap_or_default()
}
}; };
let submenu = on_action.is_some().then(|| { let number = use_state(String::new);
let open = use_state(|| false);
let number_open = use_state(|| false);
let submenu_open = open.clone();
let submenu = on_action.clone().map(|on_action| {
let pid = player.identification.player_id.clone();
let number_submit = number.clone();
let open = submenu_open.clone();
let on_number_submit = Callback::from(move |_| {
let number = match number_submit.trim().parse::<NonZeroU8>() {
Ok(num) => num,
Err(_) => return,
};
on_action.emit((pid.clone(), LobbyPlayerAction::SetNumber(number)));
open.set(false);
});
html! { html! {
<nav class="submenu"> <>
<Button on_click={(action)(LobbyPlayerAction::Kick)}>{"Kick"}</Button> <Button on_click={(action)(LobbyPlayerAction::Kick)}>{"kick"}</Button>
</nav> <ClickableNumberEdit
state={number_open}
value={number}
field_name="number"
on_submit={on_number_submit}
>
<div class="number">{"set number"}</div>
</ClickableNumberEdit>
</>
} }
}); });
html! { html! {
<div class={classes!("player", class, "column-list", "ident")}> // <div class={classes!("player", class, "column-list")}>
<ClickableField
state={open}
options={submenu}
class={classes!("player", class, "column-list")}
with_backdrop_exit=true
>
<Identity ident={player.identification.public.clone()}/> <Identity ident={player.identification.public.clone()}/>
{submenu} </ClickableField>
</div> // {submenu}
// </div>
} }
} }

View File

@ -1,21 +1,21 @@
use std::sync::Arc; use std::sync::Arc;
use werewolves_proto::message::Target; use werewolves_proto::message::CharacterIdentity;
use yew::prelude::*; use yew::prelude::*;
use crate::components::{Button, action::CustomTargetCard}; use crate::components::{Button, action::CustomTargetCard};
#[derive(Debug, PartialEq, Properties)] #[derive(Debug, PartialEq, Properties)]
pub struct RoleRevealProps { pub struct RoleRevealProps {
pub ackd: Box<[Target]>, pub ackd: Box<[CharacterIdentity]>,
pub waiting: Box<[Target]>, pub waiting: Box<[CharacterIdentity]>,
pub on_force_ready: Option<Callback<Target>>, pub on_force_ready: Option<Callback<CharacterIdentity>>,
} }
pub struct RoleReveal {} pub struct RoleReveal {}
impl Component for RoleReveal { impl Component for RoleReveal {
type Message = Target; type Message = CharacterIdentity;
type Properties = RoleRevealProps; type Properties = RoleRevealProps;
@ -34,7 +34,7 @@ impl Component for RoleReveal {
.map(|t| (t, true)) .map(|t| (t, true))
.chain(waiting.iter().map(|t| (t, false))) .chain(waiting.iter().map(|t| (t, false)))
.collect::<Box<[_]>>(); .collect::<Box<[_]>>();
chars.sort_by(|(l, _), (r, _)| l.public.number.cmp(&r.public.number)); chars.sort_by(|(l, _), (r, _)| l.number.cmp(&r.number));
let on_force_ready = on_force_ready.clone(); let on_force_ready = on_force_ready.clone();
let cards = chars let cards = chars
@ -77,9 +77,9 @@ impl Component for RoleReveal {
#[derive(Debug, PartialEq, Properties)] #[derive(Debug, PartialEq, Properties)]
pub struct RoleRevealCardProps { pub struct RoleRevealCardProps {
pub target: Target, pub target: CharacterIdentity,
pub is_ready: bool, pub is_ready: bool,
pub on_force_ready: Option<Callback<Target>>, pub on_force_ready: Option<Callback<CharacterIdentity>>,
} }
#[function_component] #[function_component]
@ -106,14 +106,6 @@ pub fn RoleRevealCard(props: &RoleRevealCardProps) -> Html {
on_select={on_click} on_select={on_click}
hide_submenu=false hide_submenu=false
/> />
// <p>{props.target.public.name.as_str()}</p>
// {
// if !props.is_ready {
// html! {<Button on_click={on_click}>{"force ready"}</Button>}
// } else {
// html!{}
// }
// }
</div> </div>
} }
} }

View File

@ -1,6 +1,11 @@
use core::{num::NonZeroU8, ops::Not};
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::{error::GameError, game::GameSettings, role::RoleTitle}; use werewolves_proto::{
error::GameError, game::GameSettings, message::PlayerState, role::RoleTitle,
};
use yew::prelude::*; use yew::prelude::*;
const ALIGN_VILLAGE: &str = "village"; const ALIGN_VILLAGE: &str = "village";
@ -9,7 +14,7 @@ const ALIGN_WOLVES: &str = "wolves";
#[derive(Debug, PartialEq, Properties)] #[derive(Debug, PartialEq, Properties)]
pub struct SettingsProps { pub struct SettingsProps {
pub settings: GameSettings, pub settings: GameSettings,
pub players_in_lobby: usize, pub players_in_lobby: Rc<[PlayerState]>,
pub on_update: Callback<GameSettings>, pub on_update: Callback<GameSettings>,
pub on_start: Callback<()>, pub on_start: Callback<()>,
#[prop_or_default] #[prop_or_default]
@ -37,15 +42,37 @@ enum AmountChange {
#[function_component] #[function_component]
pub fn Settings(props: &SettingsProps) -> Html { pub fn Settings(props: &SettingsProps) -> Html {
let on_update = props.on_update.clone(); let on_update = props.on_update.clone();
let (start_game_disabled, reason) = match props.settings.check() { let disabled_reason = match props.settings.check() {
Ok(_) => { Ok(_) => (props.players_in_lobby.len() < props.settings.min_players_needed())
if props.players_in_lobby < props.settings.min_players_needed() { .then(|| String::from("too few players for role setup"))
(true, String::from("too few players for role setup")) .or_else(|| {
props
.players_in_lobby
.iter()
.any(|p| p.identification.public.number.is_none())
.then(|| String::from("not all players are assigned numbers"))
})
.or_else(|| {
let mut unique: HashMap<NonZeroU8, NonZeroU8> = HashMap::new();
for p in props.players_in_lobby.iter() {
let num = p.identification.public.number.unwrap();
if let Some(cnt) = unique.get_mut(&num) {
*cnt = NonZeroU8::new(cnt.get() + 1).unwrap();
} else { } else {
(false, String::new()) unique.insert(num, NonZeroU8::new(1).unwrap());
} }
} }
Err(err) => (true, err.to_string()), let dupes = unique
.iter()
.filter_map(|(num, cnt)| (cnt.get() > 1).then_some(*num))
.map(|dupe| dupe.to_string())
.collect::<Box<[_]>>();
dupes
.is_empty()
.not()
.then(|| format!("duplicate numbers: {}", dupes.join(", ")))
}),
Err(err) => Some(err.to_string()),
}; };
let settings = props.settings.clone(); let settings = props.settings.clone();
let on_error = props.on_error.clone(); let on_error = props.on_error.clone();
@ -74,6 +101,7 @@ pub fn Settings(props: &SettingsProps) -> Html {
.filter(|r| !matches!(r, RoleTitle::Scapegoat | RoleTitle::Villager)) .filter(|r| !matches!(r, RoleTitle::Scapegoat | RoleTitle::Villager))
.map(|r| html! {<RoleCard role={r} amount={get_role_count(&props.settings, r)} on_changed={on_changed.clone()}/>}) .map(|r| html! {<RoleCard role={r} amount={get_role_count(&props.settings, r)} on_changed={on_changed.clone()}/>})
.collect::<Html>(); .collect::<Html>();
let disabled = disabled_reason.is_some();
html! { html! {
<div class="settings"> <div class="settings">
<h2>{format!("Min players for settings: {}", props.settings.min_players_needed())}</h2> <h2>{format!("Min players for settings: {}", props.settings.min_players_needed())}</h2>
@ -81,7 +109,7 @@ pub fn Settings(props: &SettingsProps) -> Html {
// <BoolRoleCard role={RoleTitle::Scapegoat} enabled={props.settings.scapegoat} on_changed={on_bool_changed}/> // <BoolRoleCard role={RoleTitle::Scapegoat} enabled={props.settings.scapegoat} on_changed={on_bool_changed}/>
{roles} {roles}
</div> </div>
<button reason={reason} disabled={start_game_disabled} class="start-game" onclick={on_start_game}>{"Start Game"}</button> <button reason={disabled_reason} disabled={disabled} class="start-game" onclick={on_start_game}>{"Start Game"}</button>
</div> </div>
} }
} }

View File

@ -3,7 +3,12 @@ mod clients;
mod storage; mod storage;
mod components { mod components {
werewolves_macros::include_path!("werewolves/src/components"); werewolves_macros::include_path!("werewolves/src/components");
pub mod host; pub mod client {
werewolves_macros::include_path!("werewolves/src/components/client");
}
pub mod host {
werewolves_macros::include_path!("werewolves/src/components/host");
}
pub mod action { pub mod action {
werewolves_macros::include_path!("werewolves/src/components/action"); werewolves_macros::include_path!("werewolves/src/components/action");
} }
@ -12,7 +17,6 @@ mod pages {
werewolves_macros::include_path!("werewolves/src/pages"); werewolves_macros::include_path!("werewolves/src/pages");
} }
mod callback; mod callback;
use core::num::NonZeroU8;
use pages::{ErrorComponent, WerewolfError}; use pages::{ErrorComponent, WerewolfError};
use web_sys::Url; use web_sys::Url;
@ -48,44 +52,24 @@ fn main() {
host.send_message(HostEvent::SetBigScreenState(true)); host.send_message(HostEvent::SetBigScreenState(true));
} }
} else if path.starts_with("/many-client") { } else if path.starts_with("/many-client") {
let mut number = 1..=0xFFu8; let clients = document.query_selector("clients").unwrap().unwrap();
for (player_id, name, dupe) in [ for (player_id, name, dupe) in [(
(
PlayerId::from_u128(1), PlayerId::from_u128(1),
"player 1", "player 1".to_string(),
document.query_selector("app").unwrap().unwrap(), document.query_selector("app").unwrap().unwrap(),
), )]
.into_iter()
.chain((2..17).map(|num| {
( (
PlayerId::from_u128(2), PlayerId::from_u128(num as u128),
"player 2", format!("player {num}"),
document.query_selector("dupe1").unwrap().unwrap(), document.create_element("autoclient").unwrap(),
), )
( })) {
PlayerId::from_u128(3), if dupe.tag_name() == "AUTOCLIENT" {
"player 3", clients.append_child(&dupe).unwrap();
document.query_selector("dupe2").unwrap().unwrap(), }
),
(
PlayerId::from_u128(4),
"player 4",
document.query_selector("dupe3").unwrap().unwrap(),
),
(
PlayerId::from_u128(5),
"player 5",
document.query_selector("dupe4").unwrap().unwrap(),
),
(
PlayerId::from_u128(6),
"player 6",
document.query_selector("dupe5").unwrap().unwrap(),
),
(
PlayerId::from_u128(7),
"player 7",
document.query_selector("dupe6").unwrap().unwrap(),
),
] {
let client = let client =
yew::Renderer::<Client>::with_root_and_props(dupe, ClientProps { auto_join: true }) yew::Renderer::<Client>::with_root_and_props(dupe, ClientProps { auto_join: true })
.render(); .render();
@ -94,7 +78,7 @@ fn main() {
public: PublicIdentity { public: PublicIdentity {
name: name.to_string(), name: name.to_string(),
pronouns: Some(String::from("he/him")), pronouns: Some(String::from("he/him")),
number: NonZeroU8::new(number.next().unwrap()).unwrap(), number: None,
}, },
})); }));
client.send_message(Message::SetErrorCallback(error_callback.clone())); client.send_message(Message::SetErrorCallback(error_callback.clone()));

View File

@ -1,43 +1,29 @@
use gloo::storage::{LocalStorage, Storage, errors::StorageError}; use gloo::storage::{LocalStorage, Storage, errors::StorageError};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use werewolves_proto::{message::PublicIdentity, player::PlayerId};
pub enum StorageKey { type Result<T> = core::result::Result<T, StorageError>;
PlayerId,
PublicIdentity, pub trait StorageKey: for<'a> Deserialize<'a> + Serialize {
const KEY: &str;
fn load_from_storage() -> Result<Self> {
LocalStorage::get(Self::KEY)
} }
impl StorageKey { fn save_to_storage(&self) -> Result<()> {
const fn key(&self) -> &'static str { LocalStorage::set(Self::KEY, self)
match self {
StorageKey::PlayerId => "player_id",
StorageKey::PublicIdentity => "player_public",
}
}
pub fn get<T>(&self) -> Result<T, StorageError>
where
T: for<'de> Deserialize<'de>,
{
LocalStorage::get(self.key())
} }
pub fn set<T>(&self, value: T) -> Result<(), StorageError> fn delete() {
where LocalStorage::delete(Self::KEY);
T: Serialize, }
{
LocalStorage::set(self.key(), value)
} }
pub fn get_or_set<T>(&self, value_fn: impl FnOnce() -> T) -> Result<T, StorageError> impl StorageKey for PlayerId {
where const KEY: &str = "ww_player_id";
T: Serialize + for<'de> Deserialize<'de>,
{
match LocalStorage::get(self.key()) {
Ok(v) => Ok(v),
Err(StorageError::KeyNotFound(_)) => {
LocalStorage::set(self.key(), (value_fn)())?;
self.get()
}
Err(err) => Err(err),
}
} }
impl StorageKey for PublicIdentity {
const KEY: &str = "ww_public_identity";
} }