target merge into identity, self updates, etc
This commit is contained in:
parent
862c5004fd
commit
01c1a4554a
File diff suppressed because it is too large
Load Diff
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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]>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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%;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
@ -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()}/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -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>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()));
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue