prompt ui for a few more roles + optional target
This commit is contained in:
parent
4ba77630c8
commit
09039c21c1
|
|
@ -31,8 +31,6 @@ pub enum GameError {
|
||||||
TimedOut,
|
TimedOut,
|
||||||
#[error("host channel closed")]
|
#[error("host channel closed")]
|
||||||
HostChannelClosed,
|
HostChannelClosed,
|
||||||
#[error("not all players connected")]
|
|
||||||
NotAllPlayersConnected,
|
|
||||||
#[error("too few players: got {got} but the settings require at least {need}")]
|
#[error("too few players: got {got} but the settings require at least {need}")]
|
||||||
TooFewPlayers { got: u8, need: u8 },
|
TooFewPlayers { got: u8, need: u8 },
|
||||||
#[error("it's already daytime")]
|
#[error("it's already daytime")]
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ impl Night {
|
||||||
.partial_cmp(right_prompt)
|
.partial_cmp(right_prompt)
|
||||||
.unwrap_or(core::cmp::Ordering::Equal)
|
.unwrap_or(core::cmp::Ordering::Equal)
|
||||||
});
|
});
|
||||||
let mut action_queue = VecDeque::from(action_queue);
|
let action_queue = VecDeque::from(action_queue);
|
||||||
let (current_prompt, current_char) = if night == 0 {
|
let (current_prompt, current_char) = if night == 0 {
|
||||||
(
|
(
|
||||||
ActionPrompt::WolvesIntro {
|
ActionPrompt::WolvesIntro {
|
||||||
|
|
@ -117,10 +117,18 @@ impl Night {
|
||||||
.clone(),
|
.clone(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
action_queue
|
(
|
||||||
.pop_front()
|
ActionPrompt::WolfPackKill {
|
||||||
.map(|(p, c)| (p, c.character_id().clone()))
|
living_villagers: village.living_villagers(),
|
||||||
.ok_or(GameError::NoNightActions)?
|
},
|
||||||
|
village
|
||||||
|
.living_wolf_pack_players()
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.character_id()
|
||||||
|
.clone(),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
let night_state = NightState::Active {
|
let night_state = NightState::Active {
|
||||||
current_char,
|
current_char,
|
||||||
|
|
@ -329,7 +337,9 @@ impl Night {
|
||||||
} => {}
|
} => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
new_village.to_day()?;
|
if new_village.is_game_over().is_none() {
|
||||||
|
new_village.to_day()?;
|
||||||
|
}
|
||||||
Ok(new_village)
|
Ok(new_village)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,17 +83,23 @@ impl Village {
|
||||||
.find(|c| c.character_id() == character_id)
|
.find(|c| c.character_id() == character_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wolves_count(&self) -> usize {
|
fn living_wolves_count(&self) -> usize {
|
||||||
self.characters.iter().filter(|c| c.is_wolf()).count()
|
self.characters
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.is_wolf() && c.alive())
|
||||||
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn villager_count(&self) -> usize {
|
fn living_villager_count(&self) -> usize {
|
||||||
self.characters.iter().filter(|c| c.is_village()).count()
|
self.characters
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.is_village() && c.alive())
|
||||||
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_game_over(&self) -> Option<GameOver> {
|
pub fn is_game_over(&self) -> Option<GameOver> {
|
||||||
let wolves = self.wolves_count();
|
let wolves = self.living_wolves_count();
|
||||||
let villagers = self.villager_count();
|
let villagers = self.living_villager_count();
|
||||||
|
|
||||||
if wolves == 0 {
|
if wolves == 0 {
|
||||||
return Some(GameOver::VillageWins);
|
return Some(GameOver::VillageWins);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use colored::Colorize;
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::{
|
sync::{
|
||||||
Mutex,
|
Mutex,
|
||||||
|
|
@ -28,10 +27,6 @@ impl ConnectionId {
|
||||||
pub const fn player_id(&self) -> &PlayerId {
|
pub const fn player_id(&self) -> &PlayerId {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn connect_time(&self) -> &Instant {
|
|
||||||
&self.1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -67,10 +62,6 @@ impl JoinedPlayer {
|
||||||
pub fn resubscribe_reciever(&self) -> Receiver<ServerMessage> {
|
pub fn resubscribe_reciever(&self) -> Receiver<ServerMessage> {
|
||||||
self.receiver.resubscribe()
|
self.receiver.resubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sender(&self) -> Sender<ServerMessage> {
|
|
||||||
self.sender.clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -120,16 +111,6 @@ impl JoinedPlayers {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_name(&self, player_id: &PlayerId) -> Option<String> {
|
|
||||||
self.players.lock().await.iter().find_map(|(pid, p)| {
|
|
||||||
if pid == player_id {
|
|
||||||
Some(p.name.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Disconnect the player
|
/// Disconnect the player
|
||||||
///
|
///
|
||||||
/// Will not disconnect if the player is currently in a game, allowing them to reconnect
|
/// Will not disconnect if the player is currently in a game, allowing them to reconnect
|
||||||
|
|
@ -153,11 +134,11 @@ impl JoinedPlayers {
|
||||||
|
|
||||||
pub async fn start_game_with(&self, players: &[PlayerId]) -> Result<InGameToken, GameError> {
|
pub async fn start_game_with(&self, players: &[PlayerId]) -> Result<InGameToken, GameError> {
|
||||||
let mut map = self.players.lock().await;
|
let mut map = self.players.lock().await;
|
||||||
if !players.iter().all(|p| map.contains_key(p)) {
|
|
||||||
return Err(GameError::NotAllPlayersConnected);
|
|
||||||
}
|
|
||||||
for player in players {
|
for player in players {
|
||||||
unsafe { map.get_mut(player).unwrap_unchecked() }.in_game = true;
|
if let Some(player) = map.get_mut(player) {
|
||||||
|
player.in_game = true;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(InGameToken::new(
|
Ok(InGameToken::new(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
LogError,
|
LogError,
|
||||||
communication::{Comms, lobby::LobbyComms},
|
communication::{Comms, lobby::LobbyComms},
|
||||||
connection::{InGameToken, JoinedPlayers},
|
connection::{InGameToken, JoinedPlayers},
|
||||||
lobby::{Lobby, PlayerIdSender},
|
lobby::{Lobby, LobbyPlayers},
|
||||||
runner::{IdentifiedClientMessage, Message},
|
runner::{IdentifiedClientMessage, Message},
|
||||||
};
|
};
|
||||||
use tokio::{sync::broadcast::Receiver, time::Instant};
|
use tokio::{sync::broadcast::Receiver, time::Instant};
|
||||||
|
|
@ -24,18 +24,18 @@ pub struct GameRunner {
|
||||||
game: Game,
|
game: Game,
|
||||||
comms: Comms,
|
comms: Comms,
|
||||||
connect_recv: Receiver<(PlayerId, bool)>,
|
connect_recv: Receiver<(PlayerId, bool)>,
|
||||||
player_sender: PlayerIdSender,
|
player_sender: LobbyPlayers,
|
||||||
roles_revealed: bool,
|
roles_revealed: bool,
|
||||||
joined_players: JoinedPlayers,
|
joined_players: JoinedPlayers,
|
||||||
_release_token: InGameToken,
|
// _release_token: InGameToken,
|
||||||
cover_of_darkness: bool,
|
cover_of_darkness: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameRunner {
|
impl GameRunner {
|
||||||
pub const fn new(
|
pub fn new(
|
||||||
game: Game,
|
game: Game,
|
||||||
comms: Comms,
|
comms: Comms,
|
||||||
player_sender: PlayerIdSender,
|
player_sender: LobbyPlayers,
|
||||||
connect_recv: Receiver<(PlayerId, bool)>,
|
connect_recv: Receiver<(PlayerId, bool)>,
|
||||||
joined_players: JoinedPlayers,
|
joined_players: JoinedPlayers,
|
||||||
release_token: InGameToken,
|
release_token: InGameToken,
|
||||||
|
|
@ -47,7 +47,7 @@ impl GameRunner {
|
||||||
player_sender,
|
player_sender,
|
||||||
joined_players,
|
joined_players,
|
||||||
roles_revealed: false,
|
roles_revealed: false,
|
||||||
_release_token: release_token,
|
// _release_token: release_token,
|
||||||
cover_of_darkness: true,
|
cover_of_darkness: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,10 +57,13 @@ impl GameRunner {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_lobby(self) -> Lobby {
|
pub fn into_lobby(self) -> Lobby {
|
||||||
Lobby::new(
|
// core::mem::drop(self._release_token);
|
||||||
|
let mut lobby = Lobby::new(
|
||||||
self.joined_players,
|
self.joined_players,
|
||||||
LobbyComms::new(self.comms, self.connect_recv),
|
LobbyComms::new(self.comms, self.connect_recv),
|
||||||
)
|
);
|
||||||
|
lobby.set_players_in_lobby(self.player_sender);
|
||||||
|
lobby
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn proto_game(&self) -> &Game {
|
pub const fn proto_game(&self) -> &Game {
|
||||||
|
|
@ -106,7 +109,7 @@ impl GameRunner {
|
||||||
.log_err();
|
.log_err();
|
||||||
};
|
};
|
||||||
(update_host)(&acks, &mut self.comms);
|
(update_host)(&acks, &mut self.comms);
|
||||||
let notify_of_role = |player_id: &PlayerId, village: &Village, sender: &PlayerIdSender| {
|
let notify_of_role = |player_id: &PlayerId, village: &Village, sender: &LobbyPlayers| {
|
||||||
if let Some(char) = village.character_by_player_id(player_id) {
|
if let Some(char) = village.character_by_player_id(player_id) {
|
||||||
sender
|
sender
|
||||||
.send_if_present(
|
.send_if_present(
|
||||||
|
|
@ -245,23 +248,6 @@ impl GameEnd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn end_screen(&mut self) -> Result<()> {
|
|
||||||
let result = self.result;
|
|
||||||
for char in self.game()?.game.village().characters() {
|
|
||||||
self.game()?
|
|
||||||
.player_sender
|
|
||||||
.send_if_present(char.player_id(), ServerMessage::GameOver(result))
|
|
||||||
.log_debug();
|
|
||||||
}
|
|
||||||
self.game()?
|
|
||||||
.comms
|
|
||||||
.host()
|
|
||||||
.send(ServerToHostMessage::GameOver(result))
|
|
||||||
.log_warn();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn next(&mut self) -> Option<Lobby> {
|
pub async fn next(&mut self) -> Option<Lobby> {
|
||||||
let msg = match self.game().unwrap().comms.message().await {
|
let msg = match self.game().unwrap().comms.message().await {
|
||||||
Ok(msg) => msg,
|
Ok(msg) => msg,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Lobby {
|
pub struct Lobby {
|
||||||
players_in_lobby: PlayerIdSender,
|
players_in_lobby: LobbyPlayers,
|
||||||
settings: GameSettings,
|
settings: GameSettings,
|
||||||
joined_players: JoinedPlayers,
|
joined_players: JoinedPlayers,
|
||||||
comms: Option<LobbyComms>,
|
comms: Option<LobbyComms>,
|
||||||
|
|
@ -39,10 +39,14 @@ impl Lobby {
|
||||||
joined_players,
|
joined_players,
|
||||||
comms: Some(comms),
|
comms: Some(comms),
|
||||||
settings: GameSettings::default(),
|
settings: GameSettings::default(),
|
||||||
players_in_lobby: PlayerIdSender(Vec::new()),
|
players_in_lobby: LobbyPlayers(Vec::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_players_in_lobby(&mut self, players_in_lobby: LobbyPlayers) {
|
||||||
|
self.players_in_lobby = players_in_lobby
|
||||||
|
}
|
||||||
|
|
||||||
const fn comms(&mut self) -> Result<&mut LobbyComms, GameError> {
|
const fn comms(&mut self) -> Result<&mut LobbyComms, GameError> {
|
||||||
match self.comms.as_mut() {
|
match self.comms.as_mut() {
|
||||||
Some(comms) => Ok(comms),
|
Some(comms) => Ok(comms),
|
||||||
|
|
@ -80,7 +84,7 @@ impl Lobby {
|
||||||
players.into_boxed_slice()
|
players.into_boxed_slice()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_lobby_info_to_host(&mut self) -> Result<(), GameError> {
|
pub async fn send_lobby_info_to_host(&mut self) -> Result<(), GameError> {
|
||||||
let players = self.get_lobby_player_list().await;
|
let players = self.get_lobby_player_list().await;
|
||||||
self.comms()?
|
self.comms()?
|
||||||
.host()
|
.host()
|
||||||
|
|
@ -203,7 +207,7 @@ impl Lobby {
|
||||||
return Ok(Some(GameRunner::new(
|
return Ok(Some(GameRunner::new(
|
||||||
game,
|
game,
|
||||||
comms,
|
comms,
|
||||||
self.players_in_lobby.drain(),
|
self.players_in_lobby.clone(),
|
||||||
recv,
|
recv,
|
||||||
self.joined_players.clone(),
|
self.joined_players.clone(),
|
||||||
release_token,
|
release_token,
|
||||||
|
|
@ -300,9 +304,10 @@ impl Lobby {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PlayerIdSender(Vec<(Identification, Sender<ServerMessage>)>);
|
#[derive(Clone)]
|
||||||
|
pub struct LobbyPlayers(Vec<(Identification, Sender<ServerMessage>)>);
|
||||||
|
|
||||||
impl Deref for PlayerIdSender {
|
impl Deref for LobbyPlayers {
|
||||||
type Target = Vec<(Identification, Sender<ServerMessage>)>;
|
type Target = Vec<(Identification, Sender<ServerMessage>)>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
|
|
@ -310,13 +315,13 @@ impl Deref for PlayerIdSender {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DerefMut for PlayerIdSender {
|
impl DerefMut for LobbyPlayers {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
&mut self.0
|
&mut self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlayerIdSender {
|
impl LobbyPlayers {
|
||||||
pub fn find(&self, player_id: &PlayerId) -> Option<&Sender<ServerMessage>> {
|
pub fn find(&self, player_id: &PlayerId) -> Option<&Sender<ServerMessage>> {
|
||||||
self.iter()
|
self.iter()
|
||||||
.find_map(|(id, s)| (&id.player_id == player_id).then_some(s))
|
.find_map(|(id, s)| (&id.player_id == player_id).then_some(s))
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
use core::time::Duration;
|
use core::time::Duration;
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
error::GameError,
|
|
||||||
message::{ClientMessage, Identification, host::HostMessage},
|
message::{ClientMessage, Identification, host::HostMessage},
|
||||||
player::PlayerId,
|
player::PlayerId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
LogError,
|
||||||
communication::lobby::LobbyComms,
|
communication::lobby::LobbyComms,
|
||||||
connection::JoinedPlayers,
|
connection::JoinedPlayers,
|
||||||
game::{GameEnd, GameRunner},
|
game::{GameEnd, GameRunner},
|
||||||
|
|
@ -15,14 +14,6 @@ use crate::{
|
||||||
saver::Saver,
|
saver::Saver,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
enum GameOrSendError<T> {
|
|
||||||
#[error("game error: {0}")]
|
|
||||||
GameError(#[from] GameError),
|
|
||||||
#[error("send error: {0}")]
|
|
||||||
SendError(#[from] tokio::sync::mpsc::error::SendError<T>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct IdentifiedClientMessage {
|
pub struct IdentifiedClientMessage {
|
||||||
pub identity: Identification,
|
pub identity: Identification,
|
||||||
|
|
@ -76,6 +67,7 @@ pub async fn run_game(joined_players: JoinedPlayers, comms: LobbyComms, mut save
|
||||||
RunningState::GameOver(end) => {
|
RunningState::GameOver(end) => {
|
||||||
if let Some(mut new_lobby) = end.next().await {
|
if let Some(mut new_lobby) = end.next().await {
|
||||||
new_lobby.send_lobby_info_to_clients().await;
|
new_lobby.send_lobby_info_to_clients().await;
|
||||||
|
new_lobby.send_lobby_info_to_host().await.log_debug();
|
||||||
state = RunningState::Lobby(new_lobby)
|
state = RunningState::Lobby(new_lobby)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ $village_color: rgba(0, 0, 255, 0.7);
|
||||||
$connected_color: hsl(120, 68%, 50%);
|
$connected_color: hsl(120, 68%, 50%);
|
||||||
$disconnected_color: hsl(0, 68%, 50%);
|
$disconnected_color: hsl(0, 68%, 50%);
|
||||||
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -1015,4 +1016,8 @@ error {
|
||||||
background-color: $disconnected_color;
|
background-color: $disconnected_color;
|
||||||
border: 3px solid darken($disconnected_color, 20%);
|
border: 3px solid darken($disconnected_color, 20%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.dead {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use core::fmt::Debug;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use futures::SinkExt;
|
use futures::SinkExt;
|
||||||
use yew::{html::Scope, prelude::*};
|
use yew::prelude::*;
|
||||||
|
|
||||||
pub fn send_message<T: Clone + Debug + 'static, P>(
|
pub fn send_message<T: Clone + Debug + 'static, P>(
|
||||||
msg: T,
|
msg: T,
|
||||||
|
|
@ -19,13 +19,6 @@ pub fn send_message<T: Clone + Debug + 'static, P>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mouse_event<F>(inner: F) -> Callback<MouseEvent>
|
|
||||||
where
|
|
||||||
F: Fn() + 'static,
|
|
||||||
{
|
|
||||||
Callback::from(move |_| (inner)())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_fn<T, P, F>(msg_fn: F, send: futures::channel::mpsc::Sender<T>) -> Callback<P>
|
pub fn send_fn<T, P, F>(msg_fn: F, send: futures::channel::mpsc::Sender<T>) -> Callback<P>
|
||||||
where
|
where
|
||||||
T: Clone + Debug + 'static,
|
T: Clone + Debug + 'static,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
Identity,
|
Identity,
|
||||||
action::{SingleTarget, WolvesIntro},
|
action::{SingleTarget, TargetSelection, WolvesIntro},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
|
@ -48,17 +48,20 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
}
|
}
|
||||||
ActionPrompt::Seer { living_players } => {
|
ActionPrompt::Seer { living_players } => {
|
||||||
let on_complete = props.on_complete.clone();
|
let on_complete = props.on_complete.clone();
|
||||||
let on_select = Callback::from(move |target: CharacterId| {
|
let on_select = props.big_screen.not().then(|| {
|
||||||
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
Callback::from(move |target: CharacterId| {
|
||||||
HostNightMessage::ActionResponse(ActionResponse::Seer(target)),
|
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||||
)));
|
HostNightMessage::ActionResponse(ActionResponse::Seer(target)),
|
||||||
|
)));
|
||||||
|
})
|
||||||
|
.into()
|
||||||
});
|
});
|
||||||
html! {
|
html! {
|
||||||
<div>
|
<div>
|
||||||
{ident}
|
{ident}
|
||||||
<SingleTarget
|
<SingleTarget
|
||||||
targets={living_players.clone()}
|
targets={living_players.clone()}
|
||||||
on_select={on_select}
|
target_selection={on_select}
|
||||||
headline={"check alignment"}
|
headline={"check alignment"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -85,9 +88,47 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ActionPrompt::Protector { targets } => todo!(),
|
ActionPrompt::Protector { targets } => {
|
||||||
|
let on_complete = props.on_complete.clone();
|
||||||
|
let on_select = props.big_screen.not().then(|| {
|
||||||
|
Callback::from(move |target: CharacterId| {
|
||||||
|
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||||
|
HostNightMessage::ActionResponse(ActionResponse::Protector(target)),
|
||||||
|
)));
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
<SingleTarget
|
||||||
|
targets={targets.clone()}
|
||||||
|
target_selection={on_select}
|
||||||
|
headline={"protector"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
ActionPrompt::Arcanist { living_players } => todo!(),
|
ActionPrompt::Arcanist { living_players } => todo!(),
|
||||||
ActionPrompt::Gravedigger { dead_players } => todo!(),
|
ActionPrompt::Gravedigger { dead_players } => {
|
||||||
|
let on_complete = props.on_complete.clone();
|
||||||
|
let on_select = props.big_screen.not().then(|| {
|
||||||
|
Callback::from(move |target: CharacterId| {
|
||||||
|
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||||
|
HostNightMessage::ActionResponse(ActionResponse::Gravedigger(target)),
|
||||||
|
)));
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
<SingleTarget
|
||||||
|
targets={dead_players.clone()}
|
||||||
|
target_selection={on_select}
|
||||||
|
headline={"gravedigger"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
ActionPrompt::Hunter {
|
ActionPrompt::Hunter {
|
||||||
current_target,
|
current_target,
|
||||||
living_players,
|
living_players,
|
||||||
|
|
@ -101,9 +142,62 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
previous,
|
previous,
|
||||||
living_players,
|
living_players,
|
||||||
} => todo!(),
|
} => todo!(),
|
||||||
ActionPrompt::WolfPackKill { living_villagers } => todo!(),
|
ActionPrompt::WolfPackKill { living_villagers } => {
|
||||||
|
let on_complete = props.on_complete.clone();
|
||||||
|
let on_select = props.big_screen.not().then(|| {
|
||||||
|
Callback::from(move |target: CharacterId| {
|
||||||
|
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||||
|
HostNightMessage::ActionResponse(ActionResponse::WolfPackKillVote(target)),
|
||||||
|
)));
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
<SingleTarget
|
||||||
|
targets={living_villagers.clone()}
|
||||||
|
target_selection={on_select}
|
||||||
|
headline={"wolf pack kill"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
ActionPrompt::Shapeshifter => todo!(),
|
ActionPrompt::Shapeshifter => todo!(),
|
||||||
ActionPrompt::AlphaWolf { living_villagers } => todo!(),
|
ActionPrompt::AlphaWolf { living_villagers } => {
|
||||||
ActionPrompt::DireWolf { living_players } => todo!(),
|
let on_complete = props.on_complete.clone();
|
||||||
|
let on_select = props.big_screen.not().then(|| {
|
||||||
|
Callback::from(move |target: Option<CharacterId>| {
|
||||||
|
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||||
|
HostNightMessage::ActionResponse(ActionResponse::AlphaWolf(target)),
|
||||||
|
)));
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<SingleTarget
|
||||||
|
targets={living_villagers.clone()}
|
||||||
|
target_selection={on_select}
|
||||||
|
headline={"alpha wolf target"}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActionPrompt::DireWolf { living_players } => {
|
||||||
|
let on_complete = props.on_complete.clone();
|
||||||
|
let on_select = props.big_screen.not().then(|| {
|
||||||
|
Callback::from(move |target: CharacterId| {
|
||||||
|
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||||
|
HostNightMessage::ActionResponse(ActionResponse::Direwolf(target)),
|
||||||
|
)));
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<SingleTarget
|
||||||
|
targets={living_players.clone()}
|
||||||
|
target_selection={on_select}
|
||||||
|
headline={"direwolf block target"}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,48 @@
|
||||||
use core::ops::Not;
|
use core::{fmt::Debug, marker::PhantomData, ops::Not};
|
||||||
|
|
||||||
use werewolves_proto::{message::Target, player::CharacterId};
|
use werewolves_proto::{message::Target, player::CharacterId};
|
||||||
use yew::{html::Scope, prelude::*};
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::Identity;
|
use crate::components::Identity;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum TargetSelection {
|
||||||
|
SingleOptional(Callback<Option<CharacterId>>),
|
||||||
|
Single(Callback<CharacterId>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Callback<CharacterId>> for TargetSelection {
|
||||||
|
fn from(value: Callback<CharacterId>) -> Self {
|
||||||
|
Self::Single(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Callback<Option<CharacterId>>> for TargetSelection {
|
||||||
|
fn from(value: Callback<Option<CharacterId>>) -> Self {
|
||||||
|
Self::SingleOptional(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TargetSelection {
|
||||||
|
pub const fn button_disabled(&self, selected: &[CharacterId]) -> bool {
|
||||||
|
match self {
|
||||||
|
TargetSelection::SingleOptional(_) => selected.len() > 1,
|
||||||
|
TargetSelection::Single(_) => selected.len() != 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct SingleTargetProps {
|
pub struct SingleTargetProps {
|
||||||
pub targets: Box<[Target]>,
|
pub targets: Box<[Target]>,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub headline: &'static str,
|
pub headline: &'static str,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub read_only: bool,
|
pub target_selection: Option<TargetSelection>,
|
||||||
pub on_select: Callback<CharacterId>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SingleTarget {
|
pub struct SingleTarget {
|
||||||
selected: Option<CharacterId>,
|
selected: Vec<CharacterId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for SingleTarget {
|
impl Component for SingleTarget {
|
||||||
|
|
@ -25,63 +51,99 @@ impl Component for SingleTarget {
|
||||||
type Properties = SingleTargetProps;
|
type Properties = SingleTargetProps;
|
||||||
|
|
||||||
fn create(_: &Context<Self>) -> Self {
|
fn create(_: &Context<Self>) -> Self {
|
||||||
Self { selected: None }
|
Self {
|
||||||
|
selected: Vec::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
let SingleTargetProps { read_only, headline, targets, on_select } = ctx.props();
|
let SingleTargetProps {
|
||||||
let scope = ctx.link().clone();
|
headline,
|
||||||
let card_select = Callback::from(move |target| {
|
targets,
|
||||||
scope.send_message(target);
|
target_selection,
|
||||||
});
|
} = ctx.props();
|
||||||
let targets = targets.iter().map(|t| {
|
let target_selection = target_selection.clone();
|
||||||
html!{
|
let scope = ctx.link().clone();
|
||||||
<TargetCard
|
let card_select = Callback::from(move |target| {
|
||||||
target={t.clone()}
|
scope.send_message(target);
|
||||||
selected={self.selected.as_ref().map(|sel| sel == &t.character_id).unwrap_or_default()}
|
});
|
||||||
on_select={card_select.clone()}
|
let targets = targets
|
||||||
/>
|
.iter()
|
||||||
}
|
.map(|t| {
|
||||||
}).collect::<Html>();
|
html! {
|
||||||
let headline = if headline.trim().is_empty() {
|
<TargetCard
|
||||||
html!()
|
target={t.clone()}
|
||||||
} else {
|
selected={self.selected.iter().any(|sel| sel == &t.character_id)}
|
||||||
html!(<h2>{headline}</h2>)
|
on_select={card_select.clone()}
|
||||||
};
|
/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
let headline = if headline.trim().is_empty() {
|
||||||
|
html!()
|
||||||
|
} else {
|
||||||
|
html!(<h2>{headline}</h2>)
|
||||||
|
};
|
||||||
|
|
||||||
let on_select = on_select.clone();
|
let on_click =
|
||||||
let on_click = if let Some(target) = self.selected.clone() {
|
target_selection
|
||||||
Callback::from(move |_| on_select.emit(target.clone()))
|
.as_ref()
|
||||||
} else {
|
.and_then(|target_selection| match target_selection {
|
||||||
Callback::from(|_| ())
|
TargetSelection::SingleOptional(on_click) => {
|
||||||
};
|
if self.selected.len() > 1 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let selected = self.selected.iter().next().cloned();
|
||||||
|
let on_click = on_click.clone();
|
||||||
|
Some(Callback::from(move |_| on_click.emit(selected.clone())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TargetSelection::Single(on_click) => {
|
||||||
|
if self.selected.len() != 1 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let selected = self.selected[0].clone();
|
||||||
|
let on_click = on_click.clone();
|
||||||
|
Some(Callback::from(move |_| on_click.emit(selected.clone())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let submit = read_only.not().then(|| html!{
|
let submit = target_selection.as_ref().map(|target_selection| {
|
||||||
<div class="button-container sp-ace">
|
let disabled = target_selection.button_disabled(&self.selected);
|
||||||
<button disabled={self.selected.is_none()} onclick={on_click}>{"submit"}</button>
|
html! {
|
||||||
</div>
|
<div class="button-container sp-ace">
|
||||||
});
|
<button
|
||||||
|
disabled={disabled}
|
||||||
|
onclick={on_click}
|
||||||
|
>
|
||||||
|
{"submit"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
html!{
|
html! {
|
||||||
<div class="column-list">
|
<div class="column-list">
|
||||||
{headline}
|
{headline}
|
||||||
<div class="row-list">
|
<div class="row-list">
|
||||||
{targets}
|
{targets}
|
||||||
</div>
|
</div>
|
||||||
{submit}
|
{submit}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
if let Some(selected) = self.selected.as_ref() {
|
if let Some(idx) = self
|
||||||
if selected == &msg {
|
.selected
|
||||||
self.selected = None;
|
.iter()
|
||||||
} else {
|
.enumerate()
|
||||||
self.selected = Some(msg);
|
.find_map(|(idx, c)| (c == &msg).then_some(idx))
|
||||||
}
|
{
|
||||||
|
self.selected.swap_remove(idx);
|
||||||
} else {
|
} else {
|
||||||
self.selected = Some(msg);
|
self.selected.push(msg);
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
@ -96,17 +158,17 @@ pub struct TargetCardProps {
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
fn TargetCard(props: &TargetCardProps) -> Html {
|
fn TargetCard(props: &TargetCardProps) -> Html {
|
||||||
let on_select = props.on_select.clone();
|
let on_select = props.on_select.clone();
|
||||||
let target = props.target.character_id.clone();
|
let target = props.target.character_id.clone();
|
||||||
let on_click = Callback::from(move |_| {
|
let on_click = Callback::from(move |_| {
|
||||||
on_select.emit(target.clone());
|
on_select.emit(target.clone());
|
||||||
});
|
});
|
||||||
html! {
|
html! {
|
||||||
<div
|
<div
|
||||||
class={classes!("target-card", "character", props.selected.then_some("selected"))}
|
class={classes!("target-card", "character", props.selected.then_some("selected"))}
|
||||||
onclick={on_click}
|
onclick={on_click}
|
||||||
>
|
>
|
||||||
<Identity ident={props.target.public.clone()} />
|
<Identity ident={props.target.public.clone()} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ mod callback;
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
use pages::{Client, ErrorComponent, Host, WerewolfError};
|
use pages::{Client, ErrorComponent, Host, WerewolfError};
|
||||||
use web_sys::{Element, HtmlElement, Url, wasm_bindgen::JsCast};
|
use web_sys::Url;
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
message::{Identification, PublicIdentity},
|
message::{Identification, PublicIdentity},
|
||||||
player::PlayerId,
|
player::PlayerId,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ use werewolves_proto::{
|
||||||
Target,
|
Target,
|
||||||
night::{ActionPrompt, ActionResponse, ActionResult},
|
night::{ActionPrompt, ActionResponse, ActionResult},
|
||||||
},
|
},
|
||||||
player::{Character, CharacterId, PlayerId},
|
player::PlayerId,
|
||||||
role::RoleTitle,
|
role::RoleTitle,
|
||||||
};
|
};
|
||||||
use yew::{html::Scope, prelude::*};
|
use yew::{html::Scope, prelude::*};
|
||||||
|
|
|
||||||
|
|
@ -261,22 +261,20 @@ impl Component for Host {
|
||||||
log::info!("state: {:?}", self.state);
|
log::info!("state: {:?}", self.state);
|
||||||
let content = match self.state.clone() {
|
let content = match self.state.clone() {
|
||||||
HostState::GameOver { result } => {
|
HostState::GameOver { result } => {
|
||||||
let send = self.send.clone();
|
let new_lobby = self.big_screen.not().then(|| {
|
||||||
let new_lobby = Callback::from(move |_| {
|
crate::callback::send_message(HostMessage::NewLobby, self.send.clone())
|
||||||
let send = send.clone();
|
|
||||||
yew::platform::spawn_local(async move {
|
|
||||||
if let Err(err) = send.clone().send(HostMessage::NewLobby).await {
|
|
||||||
log::error!("send new lobby: {err}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div>
|
<CoverOfDarkness
|
||||||
<p>{format!("game over: {result:?}")}</p>
|
message={match result {
|
||||||
<div class="button-container">
|
GameOver::VillageWins => "village wins",
|
||||||
<button onclick={new_lobby}>{"New Lobby"}</button>
|
GameOver::WolvesWin => "wolves win",
|
||||||
</div>
|
}}
|
||||||
</div>
|
next={new_lobby}
|
||||||
|
>
|
||||||
|
{"new lobby"}
|
||||||
|
</CoverOfDarkness>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HostState::Disconnected => html! {
|
HostState::Disconnected => html! {
|
||||||
|
|
@ -497,6 +495,12 @@ impl Component for Host {
|
||||||
players: p,
|
players: p,
|
||||||
settings: _,
|
settings: _,
|
||||||
} => *p = players,
|
} => *p = players,
|
||||||
|
HostState::GameOver { result: _ } => {
|
||||||
|
self.state = HostState::Lobby {
|
||||||
|
players,
|
||||||
|
settings: GameSettings::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
HostState::CoverOfDarkness
|
HostState::CoverOfDarkness
|
||||||
| HostState::Prompt(_, _)
|
| HostState::Prompt(_, _)
|
||||||
| HostState::Result(_, _)
|
| HostState::Result(_, _)
|
||||||
|
|
@ -505,7 +509,6 @@ impl Component for Host {
|
||||||
ackd: _,
|
ackd: _,
|
||||||
waiting: _,
|
waiting: _,
|
||||||
}
|
}
|
||||||
| HostState::GameOver { result: _ }
|
|
||||||
| HostState::Day {
|
| HostState::Day {
|
||||||
characters: _,
|
characters: _,
|
||||||
day: _,
|
day: _,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue