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,
|
||||
#[error("host channel closed")]
|
||||
HostChannelClosed,
|
||||
#[error("not all players connected")]
|
||||
NotAllPlayersConnected,
|
||||
#[error("too few players: got {got} but the settings require at least {need}")]
|
||||
TooFewPlayers { got: u8, need: u8 },
|
||||
#[error("it's already daytime")]
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ impl Night {
|
|||
.partial_cmp(right_prompt)
|
||||
.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 {
|
||||
(
|
||||
ActionPrompt::WolvesIntro {
|
||||
|
|
@ -117,10 +117,18 @@ impl Night {
|
|||
.clone(),
|
||||
)
|
||||
} else {
|
||||
action_queue
|
||||
.pop_front()
|
||||
.map(|(p, c)| (p, c.character_id().clone()))
|
||||
.ok_or(GameError::NoNightActions)?
|
||||
(
|
||||
ActionPrompt::WolfPackKill {
|
||||
living_villagers: village.living_villagers(),
|
||||
},
|
||||
village
|
||||
.living_wolf_pack_players()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.character_id()
|
||||
.clone(),
|
||||
)
|
||||
};
|
||||
let night_state = NightState::Active {
|
||||
current_char,
|
||||
|
|
@ -329,7 +337,9 @@ impl Night {
|
|||
} => {}
|
||||
}
|
||||
}
|
||||
if new_village.is_game_over().is_none() {
|
||||
new_village.to_day()?;
|
||||
}
|
||||
Ok(new_village)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,17 +83,23 @@ impl Village {
|
|||
.find(|c| c.character_id() == character_id)
|
||||
}
|
||||
|
||||
fn wolves_count(&self) -> usize {
|
||||
self.characters.iter().filter(|c| c.is_wolf()).count()
|
||||
fn living_wolves_count(&self) -> usize {
|
||||
self.characters
|
||||
.iter()
|
||||
.filter(|c| c.is_wolf() && c.alive())
|
||||
.count()
|
||||
}
|
||||
|
||||
fn villager_count(&self) -> usize {
|
||||
self.characters.iter().filter(|c| c.is_village()).count()
|
||||
fn living_villager_count(&self) -> usize {
|
||||
self.characters
|
||||
.iter()
|
||||
.filter(|c| c.is_village() && c.alive())
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn is_game_over(&self) -> Option<GameOver> {
|
||||
let wolves = self.wolves_count();
|
||||
let villagers = self.villager_count();
|
||||
let wolves = self.living_wolves_count();
|
||||
let villagers = self.living_villager_count();
|
||||
|
||||
if wolves == 0 {
|
||||
return Some(GameOver::VillageWins);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use core::num::NonZeroU8;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use colored::Colorize;
|
||||
use tokio::{
|
||||
sync::{
|
||||
Mutex,
|
||||
|
|
@ -28,10 +27,6 @@ impl ConnectionId {
|
|||
pub const fn player_id(&self) -> &PlayerId {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub const fn connect_time(&self) -> &Instant {
|
||||
&self.1
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -67,10 +62,6 @@ impl JoinedPlayer {
|
|||
pub fn resubscribe_reciever(&self) -> Receiver<ServerMessage> {
|
||||
self.receiver.resubscribe()
|
||||
}
|
||||
|
||||
pub fn sender(&self) -> Sender<ServerMessage> {
|
||||
self.sender.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
|
||||
///
|
||||
/// 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> {
|
||||
let mut map = self.players.lock().await;
|
||||
if !players.iter().all(|p| map.contains_key(p)) {
|
||||
return Err(GameError::NotAllPlayersConnected);
|
||||
}
|
||||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::{
|
|||
LogError,
|
||||
communication::{Comms, lobby::LobbyComms},
|
||||
connection::{InGameToken, JoinedPlayers},
|
||||
lobby::{Lobby, PlayerIdSender},
|
||||
lobby::{Lobby, LobbyPlayers},
|
||||
runner::{IdentifiedClientMessage, Message},
|
||||
};
|
||||
use tokio::{sync::broadcast::Receiver, time::Instant};
|
||||
|
|
@ -24,18 +24,18 @@ pub struct GameRunner {
|
|||
game: Game,
|
||||
comms: Comms,
|
||||
connect_recv: Receiver<(PlayerId, bool)>,
|
||||
player_sender: PlayerIdSender,
|
||||
player_sender: LobbyPlayers,
|
||||
roles_revealed: bool,
|
||||
joined_players: JoinedPlayers,
|
||||
_release_token: InGameToken,
|
||||
// _release_token: InGameToken,
|
||||
cover_of_darkness: bool,
|
||||
}
|
||||
|
||||
impl GameRunner {
|
||||
pub const fn new(
|
||||
pub fn new(
|
||||
game: Game,
|
||||
comms: Comms,
|
||||
player_sender: PlayerIdSender,
|
||||
player_sender: LobbyPlayers,
|
||||
connect_recv: Receiver<(PlayerId, bool)>,
|
||||
joined_players: JoinedPlayers,
|
||||
release_token: InGameToken,
|
||||
|
|
@ -47,7 +47,7 @@ impl GameRunner {
|
|||
player_sender,
|
||||
joined_players,
|
||||
roles_revealed: false,
|
||||
_release_token: release_token,
|
||||
// _release_token: release_token,
|
||||
cover_of_darkness: true,
|
||||
}
|
||||
}
|
||||
|
|
@ -57,10 +57,13 @@ impl GameRunner {
|
|||
}
|
||||
|
||||
pub fn into_lobby(self) -> Lobby {
|
||||
Lobby::new(
|
||||
// core::mem::drop(self._release_token);
|
||||
let mut lobby = Lobby::new(
|
||||
self.joined_players,
|
||||
LobbyComms::new(self.comms, self.connect_recv),
|
||||
)
|
||||
);
|
||||
lobby.set_players_in_lobby(self.player_sender);
|
||||
lobby
|
||||
}
|
||||
|
||||
pub const fn proto_game(&self) -> &Game {
|
||||
|
|
@ -106,7 +109,7 @@ impl GameRunner {
|
|||
.log_err();
|
||||
};
|
||||
(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) {
|
||||
sender
|
||||
.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> {
|
||||
let msg = match self.game().unwrap().comms.message().await {
|
||||
Ok(msg) => msg,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ use crate::{
|
|||
};
|
||||
|
||||
pub struct Lobby {
|
||||
players_in_lobby: PlayerIdSender,
|
||||
players_in_lobby: LobbyPlayers,
|
||||
settings: GameSettings,
|
||||
joined_players: JoinedPlayers,
|
||||
comms: Option<LobbyComms>,
|
||||
|
|
@ -39,10 +39,14 @@ impl Lobby {
|
|||
joined_players,
|
||||
comms: Some(comms),
|
||||
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> {
|
||||
match self.comms.as_mut() {
|
||||
Some(comms) => Ok(comms),
|
||||
|
|
@ -80,7 +84,7 @@ impl Lobby {
|
|||
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;
|
||||
self.comms()?
|
||||
.host()
|
||||
|
|
@ -203,7 +207,7 @@ impl Lobby {
|
|||
return Ok(Some(GameRunner::new(
|
||||
game,
|
||||
comms,
|
||||
self.players_in_lobby.drain(),
|
||||
self.players_in_lobby.clone(),
|
||||
recv,
|
||||
self.joined_players.clone(),
|
||||
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>)>;
|
||||
|
||||
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 {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayerIdSender {
|
||||
impl LobbyPlayers {
|
||||
pub fn find(&self, player_id: &PlayerId) -> Option<&Sender<ServerMessage>> {
|
||||
self.iter()
|
||||
.find_map(|(id, s)| (&id.player_id == player_id).then_some(s))
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
use core::time::Duration;
|
||||
|
||||
use thiserror::Error;
|
||||
use werewolves_proto::{
|
||||
error::GameError,
|
||||
message::{ClientMessage, Identification, host::HostMessage},
|
||||
player::PlayerId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
LogError,
|
||||
communication::lobby::LobbyComms,
|
||||
connection::JoinedPlayers,
|
||||
game::{GameEnd, GameRunner},
|
||||
|
|
@ -15,14 +14,6 @@ use crate::{
|
|||
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)]
|
||||
pub struct IdentifiedClientMessage {
|
||||
pub identity: Identification,
|
||||
|
|
@ -76,6 +67,7 @@ pub async fn run_game(joined_players: JoinedPlayers, comms: LobbyComms, mut save
|
|||
RunningState::GameOver(end) => {
|
||||
if let Some(mut new_lobby) = end.next().await {
|
||||
new_lobby.send_lobby_info_to_clients().await;
|
||||
new_lobby.send_lobby_info_to_host().await.log_debug();
|
||||
state = RunningState::Lobby(new_lobby)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ $village_color: rgba(0, 0, 255, 0.7);
|
|||
$connected_color: hsl(120, 68%, 50%);
|
||||
$disconnected_color: hsl(0, 68%, 50%);
|
||||
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
|
|
@ -1015,4 +1016,8 @@ error {
|
|||
background-color: $disconnected_color;
|
||||
border: 3px solid darken($disconnected_color, 20%);
|
||||
}
|
||||
|
||||
&.dead {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use core::fmt::Debug;
|
|||
use std::sync::Arc;
|
||||
|
||||
use futures::SinkExt;
|
||||
use yew::{html::Scope, prelude::*};
|
||||
use yew::prelude::*;
|
||||
|
||||
pub fn send_message<T: Clone + Debug + 'static, P>(
|
||||
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>
|
||||
where
|
||||
T: Clone + Debug + 'static,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use yew::prelude::*;
|
|||
|
||||
use crate::components::{
|
||||
Identity,
|
||||
action::{SingleTarget, WolvesIntro},
|
||||
action::{SingleTarget, TargetSelection, WolvesIntro},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
|
|
@ -48,17 +48,20 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
}
|
||||
ActionPrompt::Seer { living_players } => {
|
||||
let on_complete = props.on_complete.clone();
|
||||
let on_select = Callback::from(move |target: CharacterId| {
|
||||
let on_select = props.big_screen.not().then(|| {
|
||||
Callback::from(move |target: CharacterId| {
|
||||
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||
HostNightMessage::ActionResponse(ActionResponse::Seer(target)),
|
||||
)));
|
||||
})
|
||||
.into()
|
||||
});
|
||||
html! {
|
||||
<div>
|
||||
{ident}
|
||||
<SingleTarget
|
||||
targets={living_players.clone()}
|
||||
on_select={on_select}
|
||||
target_selection={on_select}
|
||||
headline={"check alignment"}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -85,9 +88,47 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
</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::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 {
|
||||
current_target,
|
||||
living_players,
|
||||
|
|
@ -101,9 +142,62 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
previous,
|
||||
living_players,
|
||||
} => 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::AlphaWolf { living_villagers } => todo!(),
|
||||
ActionPrompt::DireWolf { living_players } => todo!(),
|
||||
ActionPrompt::AlphaWolf { living_villagers } => {
|
||||
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 yew::{html::Scope, prelude::*};
|
||||
use yew::prelude::*;
|
||||
|
||||
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)]
|
||||
pub struct SingleTargetProps {
|
||||
pub targets: Box<[Target]>,
|
||||
#[prop_or_default]
|
||||
pub headline: &'static str,
|
||||
#[prop_or_default]
|
||||
pub read_only: bool,
|
||||
pub on_select: Callback<CharacterId>,
|
||||
pub target_selection: Option<TargetSelection>,
|
||||
}
|
||||
|
||||
pub struct SingleTarget {
|
||||
selected: Option<CharacterId>,
|
||||
selected: Vec<CharacterId>,
|
||||
}
|
||||
|
||||
impl Component for SingleTarget {
|
||||
|
|
@ -25,44 +51,79 @@ impl Component for SingleTarget {
|
|||
type Properties = SingleTargetProps;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Self { selected: None }
|
||||
Self {
|
||||
selected: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let SingleTargetProps { read_only, headline, targets, on_select } = ctx.props();
|
||||
let SingleTargetProps {
|
||||
headline,
|
||||
targets,
|
||||
target_selection,
|
||||
} = ctx.props();
|
||||
let target_selection = target_selection.clone();
|
||||
let scope = ctx.link().clone();
|
||||
let card_select = Callback::from(move |target| {
|
||||
scope.send_message(target);
|
||||
});
|
||||
let targets = targets.iter().map(|t| {
|
||||
html!{
|
||||
let targets = targets
|
||||
.iter()
|
||||
.map(|t| {
|
||||
html! {
|
||||
<TargetCard
|
||||
target={t.clone()}
|
||||
selected={self.selected.as_ref().map(|sel| sel == &t.character_id).unwrap_or_default()}
|
||||
selected={self.selected.iter().any(|sel| sel == &t.character_id)}
|
||||
on_select={card_select.clone()}
|
||||
/>
|
||||
}
|
||||
}).collect::<Html>();
|
||||
})
|
||||
.collect::<Html>();
|
||||
let headline = if headline.trim().is_empty() {
|
||||
html!()
|
||||
} else {
|
||||
html!(<h2>{headline}</h2>)
|
||||
};
|
||||
|
||||
let on_select = on_select.clone();
|
||||
let on_click = if let Some(target) = self.selected.clone() {
|
||||
Callback::from(move |_| on_select.emit(target.clone()))
|
||||
let on_click =
|
||||
target_selection
|
||||
.as_ref()
|
||||
.and_then(|target_selection| match target_selection {
|
||||
TargetSelection::SingleOptional(on_click) => {
|
||||
if self.selected.len() > 1 {
|
||||
None
|
||||
} else {
|
||||
Callback::from(|_| ())
|
||||
};
|
||||
|
||||
let submit = read_only.not().then(|| html!{
|
||||
<div class="button-container sp-ace">
|
||||
<button disabled={self.selected.is_none()} onclick={on_click}>{"submit"}</button>
|
||||
</div>
|
||||
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())))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
html!{
|
||||
let submit = target_selection.as_ref().map(|target_selection| {
|
||||
let disabled = target_selection.button_disabled(&self.selected);
|
||||
html! {
|
||||
<div class="button-container sp-ace">
|
||||
<button
|
||||
disabled={disabled}
|
||||
onclick={on_click}
|
||||
>
|
||||
{"submit"}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<div class="column-list">
|
||||
{headline}
|
||||
<div class="row-list">
|
||||
|
|
@ -74,14 +135,15 @@ impl Component for SingleTarget {
|
|||
}
|
||||
|
||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
if let Some(selected) = self.selected.as_ref() {
|
||||
if selected == &msg {
|
||||
self.selected = None;
|
||||
if let Some(idx) = self
|
||||
.selected
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(idx, c)| (c == &msg).then_some(idx))
|
||||
{
|
||||
self.selected.swap_remove(idx);
|
||||
} else {
|
||||
self.selected = Some(msg);
|
||||
}
|
||||
} else {
|
||||
self.selected = Some(msg);
|
||||
self.selected.push(msg);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ mod callback;
|
|||
use core::num::NonZeroU8;
|
||||
|
||||
use pages::{Client, ErrorComponent, Host, WerewolfError};
|
||||
use web_sys::{Element, HtmlElement, Url, wasm_bindgen::JsCast};
|
||||
use web_sys::Url;
|
||||
use werewolves_proto::{
|
||||
message::{Identification, PublicIdentity},
|
||||
player::PlayerId,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use werewolves_proto::{
|
|||
Target,
|
||||
night::{ActionPrompt, ActionResponse, ActionResult},
|
||||
},
|
||||
player::{Character, CharacterId, PlayerId},
|
||||
player::PlayerId,
|
||||
role::RoleTitle,
|
||||
};
|
||||
use yew::{html::Scope, prelude::*};
|
||||
|
|
|
|||
|
|
@ -261,22 +261,20 @@ impl Component for Host {
|
|||
log::info!("state: {:?}", self.state);
|
||||
let content = match self.state.clone() {
|
||||
HostState::GameOver { result } => {
|
||||
let send = self.send.clone();
|
||||
let new_lobby = Callback::from(move |_| {
|
||||
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}");
|
||||
}
|
||||
});
|
||||
let new_lobby = self.big_screen.not().then(|| {
|
||||
crate::callback::send_message(HostMessage::NewLobby, self.send.clone())
|
||||
});
|
||||
|
||||
html! {
|
||||
<div>
|
||||
<p>{format!("game over: {result:?}")}</p>
|
||||
<div class="button-container">
|
||||
<button onclick={new_lobby}>{"New Lobby"}</button>
|
||||
</div>
|
||||
</div>
|
||||
<CoverOfDarkness
|
||||
message={match result {
|
||||
GameOver::VillageWins => "village wins",
|
||||
GameOver::WolvesWin => "wolves win",
|
||||
}}
|
||||
next={new_lobby}
|
||||
>
|
||||
{"new lobby"}
|
||||
</CoverOfDarkness>
|
||||
}
|
||||
}
|
||||
HostState::Disconnected => html! {
|
||||
|
|
@ -497,6 +495,12 @@ impl Component for Host {
|
|||
players: p,
|
||||
settings: _,
|
||||
} => *p = players,
|
||||
HostState::GameOver { result: _ } => {
|
||||
self.state = HostState::Lobby {
|
||||
players,
|
||||
settings: GameSettings::default(),
|
||||
}
|
||||
}
|
||||
HostState::CoverOfDarkness
|
||||
| HostState::Prompt(_, _)
|
||||
| HostState::Result(_, _)
|
||||
|
|
@ -505,7 +509,6 @@ impl Component for Host {
|
|||
ackd: _,
|
||||
waiting: _,
|
||||
}
|
||||
| HostState::GameOver { result: _ }
|
||||
| HostState::Day {
|
||||
characters: _,
|
||||
day: _,
|
||||
|
|
|
|||
Loading…
Reference in New Issue