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" }
|
||||
werewolves-macros = { path = "../werewolves-macros" }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { version = "1" }
|
||||
pretty_env_logger = { version = "0.5" }
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ pub enum GameError {
|
|||
InvalidMessageForGameState,
|
||||
#[error("no executions during night time")]
|
||||
NoExecutionsAtNight,
|
||||
#[error("no-trial not allowed")]
|
||||
NoTrialNotAllowed,
|
||||
#[error("chracter is already dead")]
|
||||
CharacterAlreadyDead,
|
||||
#[error("no matching character found")]
|
||||
|
|
@ -47,8 +45,6 @@ pub enum GameError {
|
|||
CantAddVillagerToSettings,
|
||||
#[error("no mentor for an apprentice to be an apprentice to :(")]
|
||||
NoApprenticeMentor,
|
||||
#[error("BUG: cannot find character in village, but they should be there")]
|
||||
CannotFindTargetButShouldBeThere,
|
||||
#[error("inactive game object")]
|
||||
InactiveGameObject,
|
||||
#[error("socket error: {0}")]
|
||||
|
|
@ -73,4 +69,6 @@ pub enum GameError {
|
|||
NoPreviousState,
|
||||
#[error("invalid original kill for guardian guard")]
|
||||
GuardianInvalidOriginalKill,
|
||||
#[error("player not assigned number: {0}")]
|
||||
PlayerNotAssignedNumber(String),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,8 +94,7 @@ impl Game {
|
|||
.into_iter()
|
||||
.map(|c| CharacterState {
|
||||
player_id: c.player_id().clone(),
|
||||
character_id: c.character_id().clone(),
|
||||
public_identity: c.public_identity().clone(),
|
||||
identity: c.identity(),
|
||||
role: c.role().title(),
|
||||
died_to: c.died_to().cloned(),
|
||||
})
|
||||
|
|
@ -109,9 +108,7 @@ impl Game {
|
|||
(GameState::Night { night }, HostGameMessage::GetState) => {
|
||||
if let Some(res) = night.current_result() {
|
||||
return Ok(ServerToHostMessage::ActionResult(
|
||||
night
|
||||
.current_character()
|
||||
.map(|c| c.public_identity().clone()),
|
||||
night.current_character().map(|c| c.identity()),
|
||||
res.clone(),
|
||||
));
|
||||
}
|
||||
|
|
@ -138,9 +135,7 @@ impl Game {
|
|||
HostGameMessage::Night(HostNightMessage::ActionResponse(resp)),
|
||||
) => match night.received_response(resp.clone()) {
|
||||
Ok(res) => Ok(ServerToHostMessage::ActionResult(
|
||||
night
|
||||
.current_character()
|
||||
.map(|c| c.public_identity().clone()),
|
||||
night.current_character().map(|c| c.identity()),
|
||||
res,
|
||||
)),
|
||||
Err(GameError::NightNeedsNext) => match night.next() {
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ impl Night {
|
|||
wolves: village
|
||||
.living_wolf_pack_players()
|
||||
.into_iter()
|
||||
.map(|w| (w.target(), w.role().title()))
|
||||
.map(|w| (w.identity(), w.role().title()))
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
|
@ -324,7 +324,7 @@ impl Night {
|
|||
.village
|
||||
.character_by_id(&kill_target)
|
||||
.ok_or(GameError::NoMatchingCharacterFound)?
|
||||
.character_identity(),
|
||||
.identity(),
|
||||
});
|
||||
}
|
||||
// Remove any further shapeshift prompts from the queue
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ use super::Result;
|
|||
use crate::{
|
||||
error::GameError,
|
||||
game::{DateTime, GameOver, GameSettings},
|
||||
message::{Identification, Target},
|
||||
message::{CharacterIdentity, Identification},
|
||||
player::{Character, CharacterId, PlayerId},
|
||||
role::{Role, RoleTitle},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Village {
|
||||
characters: Vec<Character>,
|
||||
characters: Box<[Character]>,
|
||||
date_time: DateTime,
|
||||
}
|
||||
|
||||
|
|
@ -58,8 +58,12 @@ impl Village {
|
|||
.iter()
|
||||
.cloned()
|
||||
.zip(roles)
|
||||
.map(|(player, role)| Character::new(player, role))
|
||||
.collect(),
|
||||
.map(|(player, role)| {
|
||||
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 },
|
||||
})
|
||||
}
|
||||
|
|
@ -118,17 +122,11 @@ impl Village {
|
|||
DateTime::Night { number: _ } => return Err(GameError::NoExecutionsAtNight),
|
||||
};
|
||||
|
||||
if characters.is_empty() {
|
||||
return Err(GameError::NoTrialNotAllowed);
|
||||
}
|
||||
let targets = self
|
||||
.characters
|
||||
.iter_mut()
|
||||
.filter(|c| characters.contains(c.character_id()))
|
||||
.collect::<Box<[_]>>();
|
||||
if targets.len() != characters.len() {
|
||||
return Err(GameError::CannotFindTargetButShouldBeThere);
|
||||
}
|
||||
for t in targets {
|
||||
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
|
||||
.iter()
|
||||
.filter(|c| c.alive())
|
||||
.map(Character::target)
|
||||
.map(Character::identity)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn target_by_id(&self, character_id: &CharacterId) -> Option<Target> {
|
||||
self.character_by_id(character_id).map(Character::target)
|
||||
pub fn target_by_id(&self, character_id: &CharacterId) -> Option<CharacterIdentity> {
|
||||
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
|
||||
.iter()
|
||||
.filter(|c| c.alive() && c.is_village())
|
||||
.map(Character::target)
|
||||
.map(Character::identity)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn living_players_excluding(&self, exclude: &CharacterId) -> Box<[Target]> {
|
||||
pub fn living_players_excluding(&self, exclude: &CharacterId) -> Box<[CharacterIdentity]> {
|
||||
self.characters
|
||||
.iter()
|
||||
.filter(|c| c.alive() && c.character_id() != exclude)
|
||||
.map(Character::target)
|
||||
.map(Character::identity)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn dead_targets(&self) -> Box<[Target]> {
|
||||
pub fn dead_targets(&self) -> Box<[CharacterIdentity]> {
|
||||
self.characters
|
||||
.iter()
|
||||
.filter(|c| !c.alive())
|
||||
.map(Character::target)
|
||||
.map(Character::identity)
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ fn gen_players(range: Range<u8>) -> Box<[Identification]> {
|
|||
public: PublicIdentity {
|
||||
name: format!("player {num}"),
|
||||
pronouns: None,
|
||||
number: NonZeroU8::new(num).unwrap(),
|
||||
number: NonZeroU8::new(num),
|
||||
},
|
||||
})
|
||||
.collect()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
|||
|
||||
use crate::{
|
||||
message::{
|
||||
CharacterIdentity, PublicIdentity,
|
||||
CharacterIdentity,
|
||||
night::{ActionPrompt, ActionPromptTitle},
|
||||
},
|
||||
player::CharacterId,
|
||||
|
|
@ -13,11 +13,9 @@ use crate::{
|
|||
fn character_identity() -> CharacterIdentity {
|
||||
CharacterIdentity {
|
||||
character_id: CharacterId::new(),
|
||||
public: PublicIdentity {
|
||||
name: String::new(),
|
||||
pronouns: None,
|
||||
name: Default::default(),
|
||||
pronouns: Default::default(),
|
||||
number: NonZeroU8::new(1).unwrap(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,19 +14,3 @@ pub mod modifier;
|
|||
pub mod nonzero;
|
||||
pub mod player;
|
||||
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,
|
||||
}
|
||||
|
||||
#[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)]
|
||||
pub enum ServerMessage {
|
||||
Disconnect,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
use core::num::NonZeroU8;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use werewolves_macros::Extract;
|
||||
|
||||
use crate::{
|
||||
error::GameError,
|
||||
game::{GameOver, GameSettings},
|
||||
message::{
|
||||
PublicIdentity, Target,
|
||||
CharacterIdentity, PublicIdentity,
|
||||
night::{ActionPrompt, ActionResponse, ActionResult},
|
||||
},
|
||||
player::{CharacterId, PlayerId},
|
||||
|
|
@ -55,6 +54,7 @@ impl From<HostDayMessage> for HostGameMessage {
|
|||
pub enum HostLobbyMessage {
|
||||
GetState,
|
||||
Kick(PlayerId),
|
||||
SetPlayerNumber(PlayerId, NonZeroU8),
|
||||
GetGameSettings,
|
||||
SetGameSettings(GameSettings),
|
||||
Start,
|
||||
|
|
@ -69,13 +69,13 @@ pub enum ServerToHostMessage {
|
|||
day: NonZeroU8,
|
||||
},
|
||||
ActionPrompt(ActionPrompt),
|
||||
ActionResult(Option<PublicIdentity>, ActionResult),
|
||||
ActionResult(Option<CharacterIdentity>, ActionResult),
|
||||
Lobby(Box<[PlayerState]>),
|
||||
GameSettings(GameSettings),
|
||||
Error(GameError),
|
||||
GameOver(GameOver),
|
||||
WaitingForRoleRevealAcks {
|
||||
ackd: Box<[Target]>,
|
||||
waiting: Box<[Target]>,
|
||||
ackd: Box<[CharacterIdentity]>,
|
||||
waiting: Box<[CharacterIdentity]>,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,34 +14,70 @@ pub struct Identification {
|
|||
pub public: PublicIdentity,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub struct PublicIdentity {
|
||||
pub name: String,
|
||||
pub pronouns: Option<String>,
|
||||
pub number: NonZeroU8,
|
||||
pub number: Option<NonZeroU8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CharacterIdentity {
|
||||
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 {
|
||||
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 {
|
||||
name,
|
||||
number,
|
||||
pronouns,
|
||||
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()
|
||||
.map(|p| format!(" ({p})"))
|
||||
.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)]
|
||||
pub struct CharacterState {
|
||||
pub player_id: PlayerId,
|
||||
pub character_id: CharacterId,
|
||||
pub public_identity: PublicIdentity,
|
||||
pub identity: CharacterIdentity,
|
||||
pub role: RoleTitle,
|
||||
pub died_to: Option<DiedTo>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ use crate::{
|
|||
role::{Alignment, PreviousGuardianAction, RoleTitle},
|
||||
};
|
||||
|
||||
use super::Target;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
|
||||
pub enum ActionType {
|
||||
Cover,
|
||||
|
|
@ -40,7 +38,9 @@ pub enum ActionPrompt {
|
|||
CoverOfDarkness,
|
||||
#[checks(ActionType::WolfPackKill)]
|
||||
#[checks]
|
||||
WolvesIntro { wolves: Box<[(Target, RoleTitle)]> },
|
||||
WolvesIntro {
|
||||
wolves: Box<[(CharacterIdentity, RoleTitle)]>,
|
||||
},
|
||||
#[checks(ActionType::RoleChange)]
|
||||
RoleChange {
|
||||
character_id: CharacterIdentity,
|
||||
|
|
@ -49,59 +49,61 @@ pub enum ActionPrompt {
|
|||
#[checks(ActionType::Other)]
|
||||
Seer {
|
||||
character_id: CharacterIdentity,
|
||||
living_players: Box<[Target]>,
|
||||
living_players: Box<[CharacterIdentity]>,
|
||||
},
|
||||
#[checks(ActionType::Protect)]
|
||||
Protector {
|
||||
character_id: CharacterIdentity,
|
||||
targets: Box<[Target]>,
|
||||
targets: Box<[CharacterIdentity]>,
|
||||
},
|
||||
#[checks(ActionType::Other)]
|
||||
Arcanist {
|
||||
character_id: CharacterIdentity,
|
||||
living_players: Box<[Target]>,
|
||||
living_players: Box<[CharacterIdentity]>,
|
||||
},
|
||||
#[checks(ActionType::Other)]
|
||||
Gravedigger {
|
||||
character_id: CharacterIdentity,
|
||||
dead_players: Box<[Target]>,
|
||||
dead_players: Box<[CharacterIdentity]>,
|
||||
},
|
||||
#[checks(ActionType::Other)]
|
||||
Hunter {
|
||||
character_id: CharacterIdentity,
|
||||
current_target: Option<Target>,
|
||||
living_players: Box<[Target]>,
|
||||
current_target: Option<CharacterIdentity>,
|
||||
living_players: Box<[CharacterIdentity]>,
|
||||
},
|
||||
#[checks(ActionType::Other)]
|
||||
Militia {
|
||||
character_id: CharacterIdentity,
|
||||
living_players: Box<[Target]>,
|
||||
living_players: Box<[CharacterIdentity]>,
|
||||
},
|
||||
#[checks(ActionType::Other)]
|
||||
MapleWolf {
|
||||
character_id: CharacterIdentity,
|
||||
kill_or_die: bool,
|
||||
living_players: Box<[Target]>,
|
||||
living_players: Box<[CharacterIdentity]>,
|
||||
},
|
||||
#[checks(ActionType::Protect)]
|
||||
Guardian {
|
||||
character_id: CharacterIdentity,
|
||||
previous: Option<PreviousGuardianAction>,
|
||||
living_players: Box<[Target]>,
|
||||
living_players: Box<[CharacterIdentity]>,
|
||||
},
|
||||
#[checks(ActionType::WolfPackKill)]
|
||||
WolfPackKill { living_villagers: Box<[Target]> },
|
||||
WolfPackKill {
|
||||
living_villagers: Box<[CharacterIdentity]>,
|
||||
},
|
||||
#[checks(ActionType::OtherWolf)]
|
||||
Shapeshifter { character_id: CharacterIdentity },
|
||||
#[checks(ActionType::OtherWolf)]
|
||||
AlphaWolf {
|
||||
character_id: CharacterIdentity,
|
||||
living_villagers: Box<[Target]>,
|
||||
living_villagers: Box<[CharacterIdentity]>,
|
||||
},
|
||||
#[checks(ActionType::Direwolf)]
|
||||
DireWolf {
|
||||
character_id: CharacterIdentity,
|
||||
living_players: Box<[Target]>,
|
||||
living_players: Box<[CharacterIdentity]>,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{DateTime, Village},
|
||||
message::{CharacterIdentity, Identification, PublicIdentity, Target, night::ActionPrompt},
|
||||
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
||||
modifier::Modifier,
|
||||
role::{MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
|
||||
};
|
||||
|
|
@ -77,8 +77,7 @@ pub enum KillOutcome {
|
|||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Character {
|
||||
player_id: PlayerId,
|
||||
character_id: CharacterId,
|
||||
public: PublicIdentity,
|
||||
identity: CharacterIdentity,
|
||||
role: Role,
|
||||
modifier: Option<Modifier>,
|
||||
died_to: Option<DiedTo>,
|
||||
|
|
@ -93,46 +92,47 @@ pub struct RoleChange {
|
|||
}
|
||||
|
||||
impl Character {
|
||||
pub fn new(Identification { player_id, public }: Identification, role: Role) -> Self {
|
||||
Self {
|
||||
role,
|
||||
public,
|
||||
pub fn new(
|
||||
Identification {
|
||||
player_id,
|
||||
public:
|
||||
PublicIdentity {
|
||||
name,
|
||||
pronouns,
|
||||
number,
|
||||
},
|
||||
}: Identification,
|
||||
role: Role,
|
||||
) -> Option<Self> {
|
||||
Some(Self {
|
||||
role,
|
||||
identity: CharacterIdentity {
|
||||
character_id: CharacterId::new(),
|
||||
name,
|
||||
pronouns,
|
||||
number: number?,
|
||||
},
|
||||
player_id,
|
||||
modifier: None,
|
||||
died_to: None,
|
||||
role_changes: Vec::new(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn target(&self) -> Target {
|
||||
Target {
|
||||
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 identity(&self) -> CharacterIdentity {
|
||||
self.identity.clone()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.public.name
|
||||
self.identity.name.as_str()
|
||||
}
|
||||
|
||||
pub const fn number(&self) -> NonZeroU8 {
|
||||
self.public.number
|
||||
self.identity.number
|
||||
}
|
||||
|
||||
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()),
|
||||
None => None,
|
||||
}
|
||||
|
|
@ -162,7 +162,7 @@ impl Character {
|
|||
}
|
||||
|
||||
pub const fn character_id(&self) -> &CharacterId {
|
||||
&self.character_id
|
||||
&self.identity.character_id
|
||||
}
|
||||
|
||||
pub const fn player_id(&self) -> &PlayerId {
|
||||
|
|
@ -220,24 +220,24 @@ impl Character {
|
|||
| Role::Scapegoat
|
||||
| Role::Villager => return Ok(None),
|
||||
Role::Seer => ActionPrompt::Seer {
|
||||
character_id: self.character_identity(),
|
||||
living_players: village.living_players_excluding(&self.character_id),
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
},
|
||||
Role::Arcanist => ActionPrompt::Arcanist {
|
||||
character_id: self.character_identity(),
|
||||
living_players: village.living_players_excluding(&self.character_id),
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
},
|
||||
Role::Protector {
|
||||
last_protected: Some(last_protected),
|
||||
} => ActionPrompt::Protector {
|
||||
character_id: self.character_identity(),
|
||||
character_id: self.identity(),
|
||||
targets: village.living_players_excluding(last_protected),
|
||||
},
|
||||
Role::Protector {
|
||||
last_protected: None,
|
||||
} => ActionPrompt::Protector {
|
||||
character_id: self.character_identity(),
|
||||
targets: village.living_players_excluding(&self.character_id),
|
||||
character_id: self.identity(),
|
||||
targets: village.living_players_excluding(self.character_id()),
|
||||
},
|
||||
Role::Apprentice(role) => {
|
||||
let current_night = match village.date_time() {
|
||||
|
|
@ -254,7 +254,7 @@ impl Character {
|
|||
DateTime::Night { number } => number + 1 >= current_night,
|
||||
})
|
||||
.then(|| ActionPrompt::RoleChange {
|
||||
character_id: self.character_identity(),
|
||||
character_id: self.identity(),
|
||||
new_role: role.title(),
|
||||
}));
|
||||
}
|
||||
|
|
@ -265,61 +265,61 @@ impl Character {
|
|||
};
|
||||
return Ok((current_night == knows_on_night.get()).then_some({
|
||||
ActionPrompt::RoleChange {
|
||||
character_id: self.character_identity(),
|
||||
character_id: self.identity(),
|
||||
new_role: RoleTitle::Elder,
|
||||
}
|
||||
}));
|
||||
}
|
||||
Role::Militia { targeted: None } => ActionPrompt::Militia {
|
||||
character_id: self.character_identity(),
|
||||
living_players: village.living_players_excluding(&self.character_id),
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
},
|
||||
Role::Werewolf => ActionPrompt::WolfPackKill {
|
||||
living_villagers: village.living_players(),
|
||||
},
|
||||
Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf {
|
||||
character_id: self.character_identity(),
|
||||
living_villagers: village.living_players_excluding(&self.character_id),
|
||||
character_id: self.identity(),
|
||||
living_villagers: village.living_players_excluding(self.character_id()),
|
||||
},
|
||||
Role::DireWolf => ActionPrompt::DireWolf {
|
||||
character_id: self.character_identity(),
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players(),
|
||||
},
|
||||
Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter {
|
||||
character_id: self.character_identity(),
|
||||
character_id: self.identity(),
|
||||
},
|
||||
Role::Gravedigger => ActionPrompt::Gravedigger {
|
||||
character_id: self.character_identity(),
|
||||
character_id: self.identity(),
|
||||
dead_players: village.dead_targets(),
|
||||
},
|
||||
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)),
|
||||
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 {
|
||||
character_id: self.character_identity(),
|
||||
character_id: self.identity(),
|
||||
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 {
|
||||
last_protected: Some(PreviousGuardianAction::Guard(prev_target)),
|
||||
} => ActionPrompt::Guardian {
|
||||
character_id: self.character_identity(),
|
||||
character_id: self.identity(),
|
||||
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
|
||||
living_players: village.living_players_excluding(&prev_target.character_id),
|
||||
},
|
||||
Role::Guardian {
|
||||
last_protected: Some(PreviousGuardianAction::Protect(prev_target)),
|
||||
} => ActionPrompt::Guardian {
|
||||
character_id: self.character_identity(),
|
||||
character_id: self.identity(),
|
||||
previous: Some(PreviousGuardianAction::Protect(prev_target.clone())),
|
||||
living_players: village.living_players(),
|
||||
},
|
||||
Role::Guardian {
|
||||
last_protected: None,
|
||||
} => ActionPrompt::Guardian {
|
||||
character_id: self.character_identity(),
|
||||
character_id: self.identity(),
|
||||
previous: None,
|
||||
living_players: village.living_players(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use werewolves_macros::{ChecksAs, Titles};
|
|||
|
||||
use crate::{
|
||||
game::{DateTime, Village},
|
||||
message::Target,
|
||||
message::CharacterIdentity,
|
||||
player::CharacterId,
|
||||
};
|
||||
|
||||
|
|
@ -178,6 +178,6 @@ pub enum RoleBlock {
|
|||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum PreviousGuardianAction {
|
||||
Protect(Target),
|
||||
Guard(Target),
|
||||
Protect(CharacterIdentity),
|
||||
Guard(CharacterIdentity),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ edition = "2024"
|
|||
|
||||
[dependencies]
|
||||
axum = { version = "0.8", features = ["ws"] }
|
||||
tokio = { version = "1.44", features = ["full"] }
|
||||
tokio = { version = "1.47", features = ["full"] }
|
||||
log = { version = "0.4" }
|
||||
pretty_env_logger = { version = "0.5" }
|
||||
# env_logger = { version = "0.11" }
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ struct Client {
|
|||
who: String,
|
||||
sender: Sender<IdentifiedClientMessage>,
|
||||
receiver: Receiver<ServerMessage>,
|
||||
message_history: Vec<ServerMessage>,
|
||||
// message_history: Vec<ServerMessage>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
|
|
@ -140,7 +140,7 @@ impl Client {
|
|||
who,
|
||||
sender,
|
||||
receiver,
|
||||
message_history: Vec::new(),
|
||||
// message_history: Vec::new(),
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "cbor")]
|
||||
|
|
@ -193,7 +193,7 @@ impl Client {
|
|||
if let ClientMessage::UpdateSelf(update) = &message {
|
||||
match update {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
|
@ -221,7 +221,7 @@ impl Client {
|
|||
})
|
||||
})
|
||||
.await?;
|
||||
self.message_history.push(message);
|
||||
// self.message_history.push(message);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,25 @@
|
|||
use core::time::Duration;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use colored::Colorize;
|
||||
use tokio::{
|
||||
sync::broadcast::{Receiver, Sender},
|
||||
time::Instant,
|
||||
};
|
||||
use werewolves_proto::{
|
||||
error::GameError,
|
||||
message::{ClientMessage, ServerMessage, Target, night::ActionResponse},
|
||||
player::{Character, CharacterId, PlayerId},
|
||||
};
|
||||
use tokio::sync::broadcast::Receiver;
|
||||
use werewolves_proto::{error::GameError, player::PlayerId};
|
||||
|
||||
use crate::{connection::JoinedPlayers, runner::IdentifiedClientMessage};
|
||||
|
||||
pub struct PlayerIdComms {
|
||||
joined_players: JoinedPlayers,
|
||||
// joined_players: JoinedPlayers,
|
||||
message_recv: Receiver<IdentifiedClientMessage>,
|
||||
connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>,
|
||||
// connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>,
|
||||
}
|
||||
|
||||
impl PlayerIdComms {
|
||||
pub fn new(
|
||||
joined_players: JoinedPlayers,
|
||||
// joined_players: JoinedPlayers,
|
||||
message_recv: Receiver<IdentifiedClientMessage>,
|
||||
connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>,
|
||||
// connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
joined_players,
|
||||
// joined_players,
|
||||
message_recv,
|
||||
connect_recv,
|
||||
// connect_recv,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ pub struct JoinedPlayer {
|
|||
active_connection: ConnectionId,
|
||||
in_game: bool,
|
||||
pub name: String,
|
||||
pub number: NonZeroU8,
|
||||
pub number: Option<NonZeroU8>,
|
||||
pub pronouns: Option<String>,
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ impl JoinedPlayer {
|
|||
receiver: Receiver<ServerMessage>,
|
||||
active_connection: ConnectionId,
|
||||
name: String,
|
||||
number: NonZeroU8,
|
||||
number: Option<NonZeroU8>,
|
||||
pronouns: Option<String>,
|
||||
) -> 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
|
||||
///
|
||||
/// 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::{
|
||||
LogError,
|
||||
communication::{Comms, lobby::LobbyComms},
|
||||
connection::{InGameToken, JoinedPlayers},
|
||||
connection::JoinedPlayers,
|
||||
lobby::{Lobby, LobbyPlayers},
|
||||
runner::{IdentifiedClientMessage, Message},
|
||||
};
|
||||
|
|
@ -13,7 +13,7 @@ use werewolves_proto::{
|
|||
game::{Game, GameOver, Village},
|
||||
message::{
|
||||
ClientMessage, Identification, ServerMessage,
|
||||
host::{HostGameMessage, HostMessage, HostNightMessage, ServerToHostMessage},
|
||||
host::{HostGameMessage, HostMessage, ServerToHostMessage},
|
||||
},
|
||||
player::{Character, PlayerId},
|
||||
};
|
||||
|
|
@ -94,11 +94,11 @@ impl GameRunner {
|
|||
.send(ServerToHostMessage::WaitingForRoleRevealAcks {
|
||||
ackd: acks
|
||||
.iter()
|
||||
.filter_map(|(a, ackd)| ackd.then_some(a.target()))
|
||||
.filter_map(|(a, ackd)| ackd.then_some(a.identity()))
|
||||
.collect(),
|
||||
waiting: acks
|
||||
.iter()
|
||||
.filter_map(|(a, ackd)| ackd.not().then_some(a.target()))
|
||||
.filter_map(|(a, ackd)| ackd.not().then_some(a.identity()))
|
||||
.collect(),
|
||||
})
|
||||
.log_err();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use core::{
|
|||
ops::{Deref, DerefMut},
|
||||
time::Duration,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, os::unix::raw::pid_t};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::broadcast::Sender;
|
||||
|
|
@ -178,6 +178,21 @@ impl Lobby {
|
|||
settings.check()?;
|
||||
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)) => {
|
||||
if self.players_in_lobby.len() < self.settings.min_players_needed() {
|
||||
return Err(GameError::TooFewPlayers {
|
||||
|
|
@ -275,12 +290,19 @@ impl Lobby {
|
|||
message: ClientMessage::UpdateSelf(_),
|
||||
}) => {
|
||||
self.joined_players
|
||||
.update(&player_id, move |p| {
|
||||
p.name = public.name;
|
||||
.update(&player_id, |p| {
|
||||
p.name = public.name.clone();
|
||||
p.number = public.number;
|
||||
p.pronouns = public.pronouns;
|
||||
p.pronouns = public.pronouns.clone();
|
||||
})
|
||||
.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_host().await.log_debug();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,11 @@ async fn main() {
|
|||
let lobby_comms = LobbyComms::new(
|
||||
Comms::new(
|
||||
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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,12 +14,13 @@ web-sys = { version = "0.3", features = [
|
|||
"HtmlDivElement",
|
||||
"HtmlSelectElement",
|
||||
] }
|
||||
wasm-bindgen = { version = "=0.2.100" }
|
||||
log = "0.4"
|
||||
rand = { version = "0.9", features = ["small_rng"] }
|
||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||
uuid = { version = "*", features = ["js"] }
|
||||
yew = { version = "0.21", features = ["csr"] }
|
||||
yew-router = "0.18.0"
|
||||
yew-router = "0.18"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
gloo = "0.11"
|
||||
|
|
@ -29,8 +30,8 @@ once_cell = "1"
|
|||
chrono = { version = "0.4" }
|
||||
werewolves-macros = { path = "../werewolves-macros" }
|
||||
werewolves-proto = { path = "../werewolves-proto" }
|
||||
futures = "0.3.31"
|
||||
wasm-bindgen-futures = "0.4.50"
|
||||
futures = "0.3"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
thiserror = { version = "2" }
|
||||
convert_case = { version = "0.8" }
|
||||
ciborium = { version = "0.2", optional = true }
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@
|
|||
<body>
|
||||
<app></app>
|
||||
<clients>
|
||||
<dupe1></dupe1>
|
||||
<!-- <dupe1></dupe1>
|
||||
<dupe2></dupe2>
|
||||
<dupe3></dupe3>
|
||||
<dupe4></dupe4>
|
||||
<dupe5></dupe5>
|
||||
<dupe6></dupe6>
|
||||
<dupe6></dupe6> -->
|
||||
</clients>
|
||||
<error></error>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -54,43 +54,86 @@ nav.debug-nav {
|
|||
}
|
||||
|
||||
|
||||
.button-container {
|
||||
button {
|
||||
.default-button {
|
||||
font-size: 1.3rem;
|
||||
border: 1px solid rgba(255, 255, 255, 1);
|
||||
padding: 5px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
background-color: black;
|
||||
color: #cccccc;
|
||||
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;
|
||||
margin: 0px;
|
||||
|
||||
&.not-set {
|
||||
border: 2px solid rgba(255, 0, 0, 0.3);
|
||||
background-color: rgba(255, 0, 0, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.player:hover {
|
||||
filter: brightness(120%);
|
||||
.submenu {
|
||||
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 {
|
||||
|
|
@ -137,26 +180,20 @@ button {
|
|||
|
||||
border: none;
|
||||
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
outline: inherit;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
background-color: #000;
|
||||
|
||||
button:disabled {
|
||||
&:disabled {
|
||||
filter: grayscale(80%);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
filter: brightness(80%);
|
||||
}
|
||||
|
||||
button:disabled:hover {
|
||||
&:disabled:hover {
|
||||
filter: sepia(100%);
|
||||
}
|
||||
|
||||
button:disabled:hover::after {
|
||||
&:disabled:hover::after {
|
||||
content: attr(reason);
|
||||
position: absolute;
|
||||
margin-top: 10px;
|
||||
|
|
@ -170,8 +207,10 @@ button:disabled:hover::after {
|
|||
min-width: 50vw;
|
||||
width: fit-content;
|
||||
padding: 3px;
|
||||
z-index: 3;
|
||||
z-index: 4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.settings {
|
||||
list-style: none;
|
||||
|
|
@ -396,7 +435,6 @@ client {
|
|||
|
||||
font-family: 'Cute Font';
|
||||
// font-size: 0.7rem;
|
||||
background-color: hsl(280, 55%, 61%);
|
||||
display: flex;
|
||||
// flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
|
|
@ -405,7 +443,6 @@ client {
|
|||
padding: 30px;
|
||||
|
||||
gap: 30px;
|
||||
filter: $client_filter;
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
|
|
@ -414,17 +451,10 @@ clients {
|
|||
|
||||
font-family: 'Cute Font';
|
||||
// font-size: 0.7rem;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
font-size: 2rem;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
|
||||
gap: 10px;
|
||||
border: solid 3px;
|
||||
border-color: #432054;
|
||||
}
|
||||
|
||||
.role-reveal-cards {
|
||||
|
|
@ -550,6 +580,19 @@ clients {
|
|||
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 {
|
||||
gap: 0px;
|
||||
margin: 0px;
|
||||
|
|
@ -558,10 +601,11 @@ clients {
|
|||
.submenu {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
// display: flex;
|
||||
justify-content: center;
|
||||
// visibility: collapse;
|
||||
display: none;
|
||||
z-index: 5;
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
|
|
@ -571,6 +615,9 @@ clients {
|
|||
&.shown {
|
||||
// visibility: visible;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
// position: absolute;
|
||||
}
|
||||
|
||||
button {
|
||||
|
|
@ -617,40 +664,14 @@ error {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.player {
|
||||
|
||||
margin: 0px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
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%);
|
||||
}
|
||||
.player-list {
|
||||
padding-bottom: 80px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
// align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.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 futures::{
|
||||
|
|
@ -16,7 +16,6 @@ use werewolves_proto::{
|
|||
game::GameOver,
|
||||
message::{
|
||||
ClientMessage, DayCharacter, Identification, PlayerUpdate, PublicIdentity, ServerMessage,
|
||||
Target,
|
||||
night::{ActionPrompt, ActionResponse, ActionResult},
|
||||
},
|
||||
player::PlayerId,
|
||||
|
|
@ -25,7 +24,10 @@ use werewolves_proto::{
|
|||
use yew::{html::Scope, prelude::*};
|
||||
|
||||
use crate::{
|
||||
components::{InputName, Notification},
|
||||
components::{
|
||||
Button, Identity, Notification,
|
||||
client::{ClientNav, InputName},
|
||||
},
|
||||
storage::StorageKey,
|
||||
};
|
||||
|
||||
|
|
@ -50,8 +52,8 @@ fn url() -> String {
|
|||
format!(
|
||||
"{}client",
|
||||
option_env!("LOCAL")
|
||||
.map(|_| super::DEBUG_URL)
|
||||
.unwrap_or(super::LIVE_URL)
|
||||
.map(|_| crate::clients::DEBUG_URL)
|
||||
.unwrap_or(crate::clients::LIVE_URL)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +187,6 @@ impl Connection {
|
|||
#[derive(PartialEq)]
|
||||
pub enum ClientEvent {
|
||||
Disconnected,
|
||||
Notification(String),
|
||||
Waiting,
|
||||
ShowRole(RoleTitle),
|
||||
NotInLobby(Box<[PublicIdentity]>),
|
||||
|
|
@ -201,13 +202,17 @@ impl TryFrom<ServerMessage> for ClientEvent {
|
|||
Ok(match msg {
|
||||
ServerMessage::Disconnect => Self::Disconnected,
|
||||
ServerMessage::LobbyInfo {
|
||||
joined: false,
|
||||
players,
|
||||
} => Self::NotInLobby(players),
|
||||
ServerMessage::LobbyInfo {
|
||||
joined: true,
|
||||
players,
|
||||
} => Self::InLobby(players),
|
||||
joined,
|
||||
mut players,
|
||||
} => {
|
||||
const LAST: NonZeroU8 = NonZeroU8::new(0xFF).unwrap();
|
||||
players.sort_by(|l, r| l.number.unwrap_or(LAST).cmp(&r.number.unwrap_or(LAST)));
|
||||
let players = players.into_iter().collect();
|
||||
match joined {
|
||||
true => Self::InLobby(players),
|
||||
false => Self::NotInLobby(players),
|
||||
}
|
||||
}
|
||||
ServerMessage::GameInProgress => Self::GameInProgress,
|
||||
_ => return Err(msg),
|
||||
})
|
||||
|
|
@ -251,10 +256,9 @@ impl Component for Client {
|
|||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
gloo::utils::document().set_title("Werewolves Player");
|
||||
let player = StorageKey::PlayerId
|
||||
.get()
|
||||
let player = PlayerId::load_from_storage()
|
||||
.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 });
|
||||
|
||||
let (send, recv) = futures::channel::mpsc::channel::<ClientMessage>(100);
|
||||
|
|
@ -337,11 +341,11 @@ impl Component for Client {
|
|||
);
|
||||
html! {
|
||||
<div class="lobby">
|
||||
<Button on_click={on_click}>{"Join"}</Button>
|
||||
<p>{format!("Players in lobby: {}", players.len())}</p>
|
||||
<ul class="players">
|
||||
{players.iter().map(|p| html!{<p>{p.to_string()}</p>}).collect::<Html>()}
|
||||
</ul>
|
||||
<button onclick={on_click}>{"Join"}</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -355,11 +359,11 @@ impl Component for Client {
|
|||
);
|
||||
html! {
|
||||
<div class="lobby">
|
||||
<Button on_click={on_click}>{"Leave"}</Button>
|
||||
<p>{format!("Players in lobby: {}", players.len())}</p>
|
||||
<ul class="players">
|
||||
{players.iter().map(|p| html!{<p>{p.to_string()}</p>}).collect::<Html>()}
|
||||
</ul>
|
||||
<button onclick={on_click}>{"Leave"}</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -378,43 +382,42 @@ impl Component for Client {
|
|||
}}</p>
|
||||
</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
|
||||
.player
|
||||
.as_ref()
|
||||
.map(|player| {
|
||||
let pronouns = if let Some(pronouns) = player.public.pronouns.as_ref() {
|
||||
html! {
|
||||
<p class={"pronouns"}>{"("}{pronouns.as_str()}{")"}</p>
|
||||
}
|
||||
} else {
|
||||
html!()
|
||||
};
|
||||
html! {
|
||||
<player>
|
||||
<p>{player.public.number.get()}</p>
|
||||
<name>{player.public.name.clone()}</name>
|
||||
{pronouns}
|
||||
</player>
|
||||
<Identity ident={player.public.clone()} class="zoom">
|
||||
</Identity>
|
||||
}
|
||||
})
|
||||
.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! {
|
||||
<ClientNav message_callback={client_nav_msg_cb} />
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<>
|
||||
{nav}
|
||||
<client>
|
||||
{player}
|
||||
{content}
|
||||
</client>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -431,34 +434,45 @@ impl Component for Client {
|
|||
}
|
||||
Message::SetPublicIdentity(public) => {
|
||||
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 => {
|
||||
let res =
|
||||
StorageKey::PlayerId
|
||||
.get_or_set(PlayerId::new)
|
||||
.and_then(|player_id| {
|
||||
StorageKey::PublicIdentity
|
||||
.set(public.clone())
|
||||
.map(|_| Identification { player_id, public })
|
||||
});
|
||||
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(),
|
||||
);
|
||||
let player_id = match PlayerId::load_from_storage() {
|
||||
Ok(pid) => pid,
|
||||
Err(StorageError::KeyNotFound(_)) => {
|
||||
let pid = PlayerId::new();
|
||||
if let Err(err) = pid.save_to_storage() {
|
||||
self.error(err.into());
|
||||
return false;
|
||||
}
|
||||
pid
|
||||
}
|
||||
Err(err) => {
|
||||
self.error(err.into());
|
||||
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,
|
||||
players: _,
|
||||
} = &msg
|
||||
&& self.auto_join
|
||||
{
|
||||
if self.auto_join {
|
||||
let mut send = self.send.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
if let Err(err) = send.send(ClientMessage::Hello).await {
|
||||
|
|
@ -480,7 +494,6 @@ impl Component for Client {
|
|||
self.auto_join = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let msg = match msg.try_into() {
|
||||
Ok(event) => {
|
||||
self.current_event.replace(event);
|
||||
|
|
@ -519,7 +532,7 @@ impl Component for Client {
|
|||
ServerMessage::Sleep => self.current_event = Some(ClientEvent::Waiting),
|
||||
ServerMessage::Update(update) => match (update, self.player.as_mut()) {
|
||||
(PlayerUpdate::Number(num), Some(player)) => {
|
||||
player.public.number = num;
|
||||
player.public.number = Some(num);
|
||||
return true;
|
||||
}
|
||||
(_, None) => return false,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use core::{num::NonZeroU8, ops::Not, time::Duration};
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use futures::{
|
||||
SinkExt, StreamExt,
|
||||
|
|
@ -11,7 +12,7 @@ use werewolves_proto::{
|
|||
error::GameError,
|
||||
game::{GameOver, GameSettings},
|
||||
message::{
|
||||
CharacterState, PlayerState, PublicIdentity, Target,
|
||||
CharacterIdentity, CharacterState, PlayerState, PublicIdentity,
|
||||
host::{
|
||||
HostDayMessage, HostGameMessage, HostLobbyMessage, HostMessage, HostNightMessage,
|
||||
ServerToHostMessage,
|
||||
|
|
@ -37,8 +38,8 @@ fn url() -> String {
|
|||
format!(
|
||||
"{}host",
|
||||
option_env!("LOCAL")
|
||||
.map(|_| super::DEBUG_URL)
|
||||
.unwrap_or(super::LIVE_URL)
|
||||
.map(|_| crate::clients::DEBUG_URL)
|
||||
.unwrap_or(crate::clients::LIVE_URL)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -184,7 +185,7 @@ pub enum HostEvent {
|
|||
pub enum HostState {
|
||||
Disconnected,
|
||||
Lobby {
|
||||
players: Box<[PlayerState]>,
|
||||
players: Rc<[PlayerState]>,
|
||||
settings: GameSettings,
|
||||
},
|
||||
Day {
|
||||
|
|
@ -196,11 +197,11 @@ pub enum HostState {
|
|||
result: GameOver,
|
||||
},
|
||||
RoleReveal {
|
||||
ackd: Box<[Target]>,
|
||||
waiting: Box<[Target]>,
|
||||
ackd: Box<[CharacterIdentity]>,
|
||||
waiting: Box<[CharacterIdentity]>,
|
||||
},
|
||||
Prompt(ActionPrompt),
|
||||
Result(Option<PublicIdentity>, ActionResult),
|
||||
Result(Option<CharacterIdentity>, ActionResult),
|
||||
}
|
||||
|
||||
impl From<ServerToHostMessage> for HostEvent {
|
||||
|
|
@ -340,7 +341,7 @@ impl Component for Host {
|
|||
settings={settings}
|
||||
on_start={on_start}
|
||||
on_update={on_changed}
|
||||
players_in_lobby={players.len()}
|
||||
players_in_lobby={players.clone()}
|
||||
/>
|
||||
}
|
||||
});
|
||||
|
|
@ -352,6 +353,9 @@ impl Component for Host {
|
|||
LobbyPlayerAction::Kick => {
|
||||
HostMessage::Lobby(HostLobbyMessage::Kick(player_id))
|
||||
}
|
||||
LobbyPlayerAction::SetNumber(num) => HostMessage::Lobby(
|
||||
HostLobbyMessage::SetPlayerNumber(player_id, num),
|
||||
),
|
||||
};
|
||||
let mut send = send.clone();
|
||||
let on_error = on_error.clone();
|
||||
|
|
@ -404,7 +408,7 @@ impl Component for Host {
|
|||
HostState::RoleReveal { ackd, waiting } => {
|
||||
let send = self.send.clone();
|
||||
let on_force_ready = self.big_screen.not().then(|| {
|
||||
Callback::from(move |target: Target| {
|
||||
Callback::from(move |target: CharacterIdentity| {
|
||||
let send = send.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
if let Err(err) = send
|
||||
|
|
@ -456,7 +460,7 @@ impl Component for Host {
|
|||
result={result}
|
||||
big_screen={self.big_screen}
|
||||
on_complete={on_complete}
|
||||
ident={ident}
|
||||
ident={ident.map(|i| i.into())}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
|
@ -471,7 +475,7 @@ impl Component for Host {
|
|||
self.send.clone(),
|
||||
);
|
||||
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_prev_click}>{"previous"}</Button>
|
||||
</nav>
|
||||
|
|
@ -489,17 +493,26 @@ impl Component for Host {
|
|||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
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 {
|
||||
HostState::Lobby {
|
||||
players: p,
|
||||
settings: _,
|
||||
} => *p = players,
|
||||
} => *p = players.into_iter().collect(),
|
||||
HostState::Disconnected | HostState::GameOver { result: _ } => {
|
||||
let mut send = self.send.clone();
|
||||
let on_err = self.error_callback.clone();
|
||||
|
||||
self.state = HostState::Lobby {
|
||||
players,
|
||||
players: players.into_iter().collect(),
|
||||
settings: Default::default(),
|
||||
};
|
||||
yew::platform::spawn_local(async move {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
pub mod client {
|
||||
include!("client/client.rs");
|
||||
mod client;
|
||||
pub use client::*;
|
||||
}
|
||||
pub mod host {
|
||||
include!("host/host.rs");
|
||||
mod host;
|
||||
pub use host::*;
|
||||
}
|
||||
// mod socket;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use core::ops::Not;
|
|||
|
||||
use werewolves_proto::{
|
||||
message::{
|
||||
PublicIdentity,
|
||||
CharacterIdentity, PublicIdentity,
|
||||
host::{HostGameMessage, HostMessage, HostNightMessage},
|
||||
night::{ActionPrompt, ActionResponse},
|
||||
},
|
||||
|
|
@ -24,14 +24,15 @@ pub struct ActionPromptProps {
|
|||
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
|
||||
.big_screen
|
||||
.not()
|
||||
.then(|| {
|
||||
ident.map(|ident| {
|
||||
let ident: PublicIdentity = ident.into();
|
||||
html! {
|
||||
<Identity ident={ident.clone()}/>
|
||||
<Identity ident={ident}/>
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -85,7 +86,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
});
|
||||
html! {
|
||||
<div>
|
||||
{identity_html(props, Some(&character_id.public))}
|
||||
{identity_html(props, Some(&character_id))}
|
||||
<SingleTarget
|
||||
targets={living_players.clone()}
|
||||
target_selection={on_select}
|
||||
|
|
@ -111,7 +112,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
});
|
||||
html! {
|
||||
<div>
|
||||
{identity_html(props, Some(&character_id.public))}
|
||||
{identity_html(props, Some(&character_id))}
|
||||
<h2>{"your role has changed"}</h2>
|
||||
<p>{new_role.to_string()}</p>
|
||||
{cont}
|
||||
|
|
@ -132,7 +133,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
});
|
||||
html! {
|
||||
<div>
|
||||
{identity_html(props, Some(&character_id.public))}
|
||||
{identity_html(props, Some(&character_id))}
|
||||
<SingleTarget
|
||||
targets={targets.clone()}
|
||||
target_selection={on_select}
|
||||
|
|
@ -155,7 +156,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
});
|
||||
html! {
|
||||
<div>
|
||||
{identity_html(props, Some(&character_id.public))}
|
||||
{identity_html(props, Some(&character_id))}
|
||||
<TwoTarget
|
||||
targets={living_players.clone()}
|
||||
target_selection={on_select}
|
||||
|
|
@ -178,7 +179,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
});
|
||||
html! {
|
||||
<div>
|
||||
{identity_html(props, Some(&character_id.public))}
|
||||
{identity_html(props, Some(&character_id))}
|
||||
<SingleTarget
|
||||
targets={dead_players.clone()}
|
||||
target_selection={on_select}
|
||||
|
|
@ -202,7 +203,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
});
|
||||
html! {
|
||||
<div>
|
||||
{identity_html(props, Some(&character_id.public))}
|
||||
{identity_html(props, Some(&character_id))}
|
||||
<SingleTarget
|
||||
targets={living_players.clone()}
|
||||
target_selection={on_select}
|
||||
|
|
@ -210,7 +211,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
>
|
||||
<h3>
|
||||
<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>})}
|
||||
</h3>
|
||||
</SingleTarget>
|
||||
|
|
@ -231,7 +232,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
});
|
||||
html! {
|
||||
<div>
|
||||
{identity_html(props, Some(&character_id.public))}
|
||||
{identity_html(props, Some(&character_id))}
|
||||
<OptionalSingleTarget
|
||||
targets={living_players.clone()}
|
||||
target_selection={on_select}
|
||||
|
|
@ -260,7 +261,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
});
|
||||
html! {
|
||||
<div>
|
||||
{identity_html(props, Some(&character_id.public))}
|
||||
{identity_html(props, Some(&character_id))}
|
||||
<OptionalSingleTarget
|
||||
targets={living_players.clone()}
|
||||
target_selection={on_select}
|
||||
|
|
@ -281,14 +282,14 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
html! {
|
||||
<>
|
||||
<b>{"last night you protected: "}</b>
|
||||
<Identity ident={target.public.clone()}/>
|
||||
<Identity ident={Into::<PublicIdentity>::into(target)}/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
PreviousGuardianAction::Guard(target) => html! {
|
||||
<>
|
||||
<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! {
|
||||
<div>
|
||||
{identity_html(props, Some(&character_id.public))}
|
||||
{identity_html(props, Some(&character_id))}
|
||||
<SingleTarget
|
||||
targets={living_players.clone()}
|
||||
target_selection={on_select}
|
||||
|
|
@ -342,7 +343,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
});
|
||||
html! {
|
||||
<div>
|
||||
{identity_html(props, Some(&character_id.public))}
|
||||
{identity_html(props, Some(&character_id))}
|
||||
<BinaryChoice on_chosen={on_select}>
|
||||
<h2>{"shapeshift?"}</h2>
|
||||
</BinaryChoice>
|
||||
|
|
@ -363,7 +364,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
});
|
||||
html! {
|
||||
<div>
|
||||
{identity_html(props, Some(&character_id.public))}
|
||||
{identity_html(props, Some(&character_id))}
|
||||
<OptionalSingleTarget
|
||||
targets={living_villagers.clone()}
|
||||
target_selection={on_select}
|
||||
|
|
@ -386,7 +387,7 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
});
|
||||
html! {
|
||||
<div>
|
||||
{identity_html(props, Some(&character_id.public))}
|
||||
{identity_html(props, Some(&character_id))}
|
||||
<SingleTarget
|
||||
targets={living_players.clone()}
|
||||
target_selection={on_select}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,17 @@ use core::{fmt::Debug, ops::Not};
|
|||
use std::sync::Arc;
|
||||
|
||||
use werewolves_macros::ChecksAs;
|
||||
use werewolves_proto::{message::Target, player::CharacterId};
|
||||
use werewolves_proto::{
|
||||
message::{CharacterIdentity, PublicIdentity},
|
||||
player::CharacterId,
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Button, Identity};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct TwoTargetProps {
|
||||
pub targets: Box<[Target]>,
|
||||
pub targets: Box<[CharacterIdentity]>,
|
||||
#[prop_or_default]
|
||||
pub headline: &'static str,
|
||||
#[prop_or_default]
|
||||
|
|
@ -52,7 +55,7 @@ impl Component for TwoTarget {
|
|||
target_selection,
|
||||
} = ctx.props();
|
||||
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 scope = ctx.link().clone();
|
||||
|
|
@ -136,7 +139,7 @@ impl Component for TwoTarget {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct OptionalSingleTargetProps {
|
||||
pub targets: Box<[Target]>,
|
||||
pub targets: Box<[CharacterIdentity]>,
|
||||
#[prop_or_default]
|
||||
pub headline: &'static str,
|
||||
#[prop_or_default]
|
||||
|
|
@ -164,7 +167,7 @@ impl Component for OptionalSingleTarget {
|
|||
children,
|
||||
} = ctx.props();
|
||||
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 scope = ctx.link().clone();
|
||||
|
|
@ -233,7 +236,7 @@ impl Component for OptionalSingleTarget {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct SingleTargetProps {
|
||||
pub targets: Box<[Target]>,
|
||||
pub targets: Box<[CharacterIdentity]>,
|
||||
#[prop_or_default]
|
||||
pub headline: &'static str,
|
||||
#[prop_or_default]
|
||||
|
|
@ -263,7 +266,7 @@ impl Component for SingleTarget {
|
|||
children,
|
||||
} = ctx.props();
|
||||
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 scope = ctx.link().clone();
|
||||
let card_select = Callback::from(move |target| {
|
||||
|
|
@ -335,7 +338,7 @@ impl Component for SingleTarget {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct TargetCardProps {
|
||||
pub target: Target,
|
||||
pub target: CharacterIdentity,
|
||||
pub selected: bool,
|
||||
pub on_select: Callback<CharacterId>,
|
||||
}
|
||||
|
|
@ -357,7 +360,7 @@ fn TargetCard(props: &TargetCardProps) -> Html {
|
|||
html! {
|
||||
<div class={"row-list baseline margin-5"}>
|
||||
<div class={classes!("player", "ident", "column-list", marked)}>
|
||||
<Identity ident={props.target.public.clone()} />
|
||||
<Identity ident={Into::<PublicIdentity>::into(&props.target)} />
|
||||
{submenu}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -366,7 +369,7 @@ fn TargetCard(props: &TargetCardProps) -> Html {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct CustomTargetCardProps {
|
||||
pub target: Target,
|
||||
pub target: CharacterIdentity,
|
||||
pub options: Arc<[String]>,
|
||||
pub on_select: Option<Callback<(CharacterId, String)>>,
|
||||
#[prop_or_default]
|
||||
|
|
@ -414,7 +417,7 @@ pub fn CustomTargetCard(
|
|||
html! {
|
||||
<div class={"row-list baseline"}>
|
||||
<div class={classes!("ident", "column-list", class)}>
|
||||
<Identity ident={target.public.clone()} />
|
||||
<Identity ident={Into::<PublicIdentity>::into(target)} />
|
||||
{submenu}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
use core::ops::Not;
|
||||
|
||||
use werewolves_proto::{message::Target, role::RoleTitle};
|
||||
use werewolves_proto::{
|
||||
message::{CharacterIdentity, PublicIdentity},
|
||||
role::RoleTitle,
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Button, Identity};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct WolvesIntroProps {
|
||||
pub wolves: Box<[(Target, RoleTitle)]>,
|
||||
pub wolves: Box<[(CharacterIdentity, RoleTitle)]>,
|
||||
pub big_screen: bool,
|
||||
pub on_complete: Callback<()>,
|
||||
}
|
||||
|
|
@ -24,7 +27,7 @@ pub fn WolvesIntro(props: &WolvesIntroProps) -> Html {
|
|||
props.wolves.iter().map(|w| html!{
|
||||
<div class="character wolves">
|
||||
<p class="role">{w.1.to_string()}</p>
|
||||
<Identity ident={w.0.public.clone()} />
|
||||
<Identity ident={Into::<PublicIdentity>::into(&w.0)} />
|
||||
</div>
|
||||
}).collect::<Html>()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ pub fn Button(props: &ButtonProperties) -> Html {
|
|||
let on_click = props.on_click.clone();
|
||||
let on_click = Callback::from(move |_| on_click.emit(()));
|
||||
html! {
|
||||
<div class="button-container">
|
||||
<button
|
||||
class="default-button"
|
||||
disabled={props.disabled_reason.is_some()}
|
||||
|
|
@ -23,6 +22,5 @@ pub fn Button(props: &ButtonProperties) -> Html {
|
|||
>
|
||||
{props.children.clone()}
|
||||
</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 werewolves_proto::{message::CharacterState, player::CharacterId};
|
||||
use werewolves_proto::{
|
||||
message::{CharacterState, PublicIdentity},
|
||||
player::CharacterId,
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Button, Identity};
|
||||
|
|
@ -26,14 +29,14 @@ pub fn DaytimePlayerList(
|
|||
) -> Html {
|
||||
let on_select = big_screen.not().then(|| on_mark.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
|
||||
.iter()
|
||||
.map(|c| {
|
||||
html! {
|
||||
<DaytimePlayer
|
||||
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()}
|
||||
/>
|
||||
}
|
||||
|
|
@ -77,10 +80,9 @@ pub fn DaytimePlayer(
|
|||
character:
|
||||
CharacterState {
|
||||
player_id: _,
|
||||
character_id,
|
||||
public_identity,
|
||||
role: _,
|
||||
died_to,
|
||||
identity,
|
||||
},
|
||||
}: &DaytimePlayerProps,
|
||||
) -> Html {
|
||||
|
|
@ -89,7 +91,7 @@ pub fn DaytimePlayer(
|
|||
let on_the_block = on_the_block.then_some("marked");
|
||||
let submenu = died_to.is_none().then_some(()).and_then(|_| {
|
||||
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_click = Callback::from(move |_| on_select.emit(character_id.clone()));
|
||||
html! {
|
||||
|
|
@ -100,9 +102,10 @@ pub fn DaytimePlayer(
|
|||
})
|
||||
});
|
||||
|
||||
let identity: PublicIdentity = identity.into();
|
||||
html! {
|
||||
<div class={classes!("player", dead, on_the_block, "column-list", "ident")}>
|
||||
<Identity ident={public_identity.clone()}/>
|
||||
<Identity ident={identity}/>
|
||||
{submenu}
|
||||
</div>
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
use core::ops::Deref;
|
||||
use werewolves_proto::message::PublicIdentity;
|
||||
use yew::prelude::*;
|
||||
|
||||
|
|
@ -24,11 +25,29 @@ pub fn Identity(props: &IdentityProps) -> Html {
|
|||
<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! {
|
||||
<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>
|
||||
{pronouns}
|
||||
</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 yew::prelude::*;
|
||||
|
||||
|
|
@ -5,23 +8,21 @@ use crate::components::{LobbyPlayer, LobbyPlayerAction};
|
|||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct LobbyProps {
|
||||
pub players: Box<[PlayerState]>,
|
||||
pub players: Rc<[PlayerState]>,
|
||||
#[prop_or_default]
|
||||
pub on_action: Option<Callback<(PlayerId, LobbyPlayerAction)>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
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! {
|
||||
<div class="column-list">
|
||||
<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
|
||||
.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>()
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
use core::num::NonZeroU8;
|
||||
|
||||
use web_sys::{HtmlDivElement, HtmlElement};
|
||||
use werewolves_proto::{message::PlayerState, player::PlayerId};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Button, Identity};
|
||||
use crate::components::{Button, ClickableField, ClickableNumberEdit, Identity};
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct LobbyPlayerProps {
|
||||
|
|
@ -14,6 +16,7 @@ pub struct LobbyPlayerProps {
|
|||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum LobbyPlayerAction {
|
||||
Kick,
|
||||
SetNumber(NonZeroU8),
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
|
|
@ -26,25 +29,54 @@ pub fn LobbyPlayer(LobbyPlayerProps { player, on_action }: &LobbyPlayerProps) ->
|
|||
let pid = player.identification.player_id.clone();
|
||||
let action = |action: LobbyPlayerAction| {
|
||||
let pid = pid.clone();
|
||||
if let Some(on_action) = on_action.as_ref() {
|
||||
let on_action = on_action.clone();
|
||||
Callback::from(move |_| on_action.emit((pid.clone(), action)))
|
||||
} else {
|
||||
Callback::noop()
|
||||
}
|
||||
on_action
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.map(|on_action| Callback::from(move |_| on_action.emit((pid.clone(), action))))
|
||||
.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! {
|
||||
<nav class="submenu">
|
||||
<Button on_click={(action)(LobbyPlayerAction::Kick)}>{"Kick"}</Button>
|
||||
</nav>
|
||||
<>
|
||||
<Button on_click={(action)(LobbyPlayerAction::Kick)}>{"kick"}</Button>
|
||||
<ClickableNumberEdit
|
||||
state={number_open}
|
||||
value={number}
|
||||
field_name="number"
|
||||
on_submit={on_number_submit}
|
||||
>
|
||||
<div class="number">{"set number"}</div>
|
||||
</ClickableNumberEdit>
|
||||
</>
|
||||
}
|
||||
});
|
||||
|
||||
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()}/>
|
||||
{submenu}
|
||||
</div>
|
||||
</ClickableField>
|
||||
// {submenu}
|
||||
// </div>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use werewolves_proto::message::Target;
|
||||
use werewolves_proto::message::CharacterIdentity;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Button, action::CustomTargetCard};
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct RoleRevealProps {
|
||||
pub ackd: Box<[Target]>,
|
||||
pub waiting: Box<[Target]>,
|
||||
pub on_force_ready: Option<Callback<Target>>,
|
||||
pub ackd: Box<[CharacterIdentity]>,
|
||||
pub waiting: Box<[CharacterIdentity]>,
|
||||
pub on_force_ready: Option<Callback<CharacterIdentity>>,
|
||||
}
|
||||
|
||||
pub struct RoleReveal {}
|
||||
|
||||
impl Component for RoleReveal {
|
||||
type Message = Target;
|
||||
type Message = CharacterIdentity;
|
||||
|
||||
type Properties = RoleRevealProps;
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ impl Component for RoleReveal {
|
|||
.map(|t| (t, true))
|
||||
.chain(waiting.iter().map(|t| (t, false)))
|
||||
.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 cards = chars
|
||||
|
|
@ -77,9 +77,9 @@ impl Component for RoleReveal {
|
|||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct RoleRevealCardProps {
|
||||
pub target: Target,
|
||||
pub target: CharacterIdentity,
|
||||
pub is_ready: bool,
|
||||
pub on_force_ready: Option<Callback<Target>>,
|
||||
pub on_force_ready: Option<Callback<CharacterIdentity>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
|
|
@ -106,14 +106,6 @@ pub fn RoleRevealCard(props: &RoleRevealCardProps) -> Html {
|
|||
on_select={on_click}
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
use core::{num::NonZeroU8, ops::Not};
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
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::*;
|
||||
|
||||
const ALIGN_VILLAGE: &str = "village";
|
||||
|
|
@ -9,7 +14,7 @@ const ALIGN_WOLVES: &str = "wolves";
|
|||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct SettingsProps {
|
||||
pub settings: GameSettings,
|
||||
pub players_in_lobby: usize,
|
||||
pub players_in_lobby: Rc<[PlayerState]>,
|
||||
pub on_update: Callback<GameSettings>,
|
||||
pub on_start: Callback<()>,
|
||||
#[prop_or_default]
|
||||
|
|
@ -37,15 +42,37 @@ enum AmountChange {
|
|||
#[function_component]
|
||||
pub fn Settings(props: &SettingsProps) -> Html {
|
||||
let on_update = props.on_update.clone();
|
||||
let (start_game_disabled, reason) = match props.settings.check() {
|
||||
Ok(_) => {
|
||||
if props.players_in_lobby < props.settings.min_players_needed() {
|
||||
(true, String::from("too few players for role setup"))
|
||||
let disabled_reason = match props.settings.check() {
|
||||
Ok(_) => (props.players_in_lobby.len() < props.settings.min_players_needed())
|
||||
.then(|| 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 {
|
||||
(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 on_error = props.on_error.clone();
|
||||
|
|
@ -74,6 +101,7 @@ pub fn Settings(props: &SettingsProps) -> Html {
|
|||
.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()}/>})
|
||||
.collect::<Html>();
|
||||
let disabled = disabled_reason.is_some();
|
||||
html! {
|
||||
<div class="settings">
|
||||
<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}/>
|
||||
{roles}
|
||||
</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>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@ mod clients;
|
|||
mod storage;
|
||||
mod 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 {
|
||||
werewolves_macros::include_path!("werewolves/src/components/action");
|
||||
}
|
||||
|
|
@ -12,7 +17,6 @@ mod pages {
|
|||
werewolves_macros::include_path!("werewolves/src/pages");
|
||||
}
|
||||
mod callback;
|
||||
use core::num::NonZeroU8;
|
||||
|
||||
use pages::{ErrorComponent, WerewolfError};
|
||||
use web_sys::Url;
|
||||
|
|
@ -48,44 +52,24 @@ fn main() {
|
|||
host.send_message(HostEvent::SetBigScreenState(true));
|
||||
}
|
||||
} else if path.starts_with("/many-client") {
|
||||
let mut number = 1..=0xFFu8;
|
||||
for (player_id, name, dupe) in [
|
||||
(
|
||||
let clients = document.query_selector("clients").unwrap().unwrap();
|
||||
for (player_id, name, dupe) in [(
|
||||
PlayerId::from_u128(1),
|
||||
"player 1",
|
||||
"player 1".to_string(),
|
||||
document.query_selector("app").unwrap().unwrap(),
|
||||
),
|
||||
)]
|
||||
.into_iter()
|
||||
.chain((2..17).map(|num| {
|
||||
(
|
||||
PlayerId::from_u128(2),
|
||||
"player 2",
|
||||
document.query_selector("dupe1").unwrap().unwrap(),
|
||||
),
|
||||
(
|
||||
PlayerId::from_u128(3),
|
||||
"player 3",
|
||||
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(),
|
||||
),
|
||||
] {
|
||||
PlayerId::from_u128(num as u128),
|
||||
format!("player {num}"),
|
||||
document.create_element("autoclient").unwrap(),
|
||||
)
|
||||
})) {
|
||||
if dupe.tag_name() == "AUTOCLIENT" {
|
||||
clients.append_child(&dupe).unwrap();
|
||||
}
|
||||
|
||||
let client =
|
||||
yew::Renderer::<Client>::with_root_and_props(dupe, ClientProps { auto_join: true })
|
||||
.render();
|
||||
|
|
@ -94,7 +78,7 @@ fn main() {
|
|||
public: PublicIdentity {
|
||||
name: name.to_string(),
|
||||
pronouns: Some(String::from("he/him")),
|
||||
number: NonZeroU8::new(number.next().unwrap()).unwrap(),
|
||||
number: None,
|
||||
},
|
||||
}));
|
||||
client.send_message(Message::SetErrorCallback(error_callback.clone()));
|
||||
|
|
|
|||
|
|
@ -1,43 +1,29 @@
|
|||
use gloo::storage::{LocalStorage, Storage, errors::StorageError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use werewolves_proto::{message::PublicIdentity, player::PlayerId};
|
||||
|
||||
pub enum StorageKey {
|
||||
PlayerId,
|
||||
PublicIdentity,
|
||||
type Result<T> = core::result::Result<T, StorageError>;
|
||||
|
||||
pub trait StorageKey: for<'a> Deserialize<'a> + Serialize {
|
||||
const KEY: &str;
|
||||
|
||||
fn load_from_storage() -> Result<Self> {
|
||||
LocalStorage::get(Self::KEY)
|
||||
}
|
||||
|
||||
impl StorageKey {
|
||||
const fn key(&self) -> &'static str {
|
||||
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())
|
||||
fn save_to_storage(&self) -> Result<()> {
|
||||
LocalStorage::set(Self::KEY, self)
|
||||
}
|
||||
|
||||
pub fn set<T>(&self, value: T) -> Result<(), StorageError>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
LocalStorage::set(self.key(), value)
|
||||
fn delete() {
|
||||
LocalStorage::delete(Self::KEY);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_or_set<T>(&self, value_fn: impl FnOnce() -> T) -> Result<T, StorageError>
|
||||
where
|
||||
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 PlayerId {
|
||||
const KEY: &str = "ww_player_id";
|
||||
}
|
||||
|
||||
impl StorageKey for PublicIdentity {
|
||||
const KEY: &str = "ww_public_identity";
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue