prompt ui for a few more roles + optional target

This commit is contained in:
emilis 2025-09-26 21:15:52 +01:00
parent 4ba77630c8
commit 09039c21c1
No known key found for this signature in database
14 changed files with 321 additions and 186 deletions

View File

@ -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")]

View File

@ -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)
} }

View File

@ -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);

View File

@ -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(

View File

@ -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,

View File

@ -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))

View File

@ -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)
} }
} }

View File

@ -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%);
}
} }

View File

@ -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,

View File

@ -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"}
/>
}
}
} }
} }

View File

@ -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()}
}; />
}
let on_select = on_select.clone(); })
let on_click = if let Some(target) = self.selected.clone() { .collect::<Html>();
Callback::from(move |_| on_select.emit(target.clone())) let headline = if headline.trim().is_empty() {
} else { html!()
Callback::from(|_| ()) } else {
}; html!(<h2>{headline}</h2>)
};
let submit = read_only.not().then(|| html!{ let on_click =
<div class="button-container sp-ace"> target_selection
<button disabled={self.selected.is_none()} onclick={on_click}>{"submit"}</button> .as_ref()
</div> .and_then(|target_selection| match target_selection {
}); TargetSelection::SingleOptional(on_click) => {
if self.selected.len() > 1 {
html!{ None
<div class="column-list"> } else {
{headline} let selected = self.selected.iter().next().cloned();
<div class="row-list"> let on_click = on_click.clone();
{targets} Some(Callback::from(move |_| on_click.emit(selected.clone())))
</div> }
{submit} }
</div> 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 = 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">
{targets}
</div>
{submit}
</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>
} }
} }

View File

@ -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,

View File

@ -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::*};

View File

@ -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: _,