player list styling

This commit is contained in:
emilis 2025-10-03 03:49:05 +01:00
parent 8c2791054a
commit cbeee94113
No known key found for this signature in database
11 changed files with 198 additions and 207 deletions

View File

@ -28,12 +28,12 @@ pub async fn handler(
.map(|x| x.to_string()) .map(|x| x.to_string())
.unwrap_or_else(|| addr.to_string()) .unwrap_or_else(|| addr.to_string())
.italic(); .italic();
log::info!( // log::debug!(
"{who}{} connected.", // "{who}{} connected.",
user_agent // user_agent
.map(|agent| format!(" (User-Agent: {})", agent.as_str())) // .map(|agent| format!(" (User-Agent: {})", agent.as_str()))
.unwrap_or_default(), // .unwrap_or_default(),
); // );
let player_list = state.joined_players; let player_list = state.joined_players;
// finalize the upgrade process by returning upgrade callback. // finalize the upgrade process by returning upgrade callback.
@ -46,7 +46,7 @@ pub async fn handler(
return; return;
} }
}; };
log::info!("connected {who} as {ident}"); // log::debug!("connected {who} as {ident}");
let connection_id = ConnectionId::new(ident.player_id.clone()); let connection_id = ConnectionId::new(ident.player_id.clone());
let recv = { let recv = {
let (send, recv) = tokio::sync::broadcast::channel(100); let (send, recv) = tokio::sync::broadcast::channel(100);
@ -76,7 +76,7 @@ pub async fn handler(
.run() .run()
.await; .await;
log::info!("ending connection with {who}"); // log::debug!("ending connection with {who}");
player_list.disconnect(&connection_id).await; player_list.disconnect(&connection_id).await;
}) })
} }
@ -178,11 +178,11 @@ impl Client {
} }
Message::Pong(_) => return Ok(()), Message::Pong(_) => return Ok(()),
Message::Close(Some(close_frame)) => { Message::Close(Some(close_frame)) => {
log::debug!("sent close frame: {close_frame:?}"); // log::debug!("sent close frame: {close_frame:?}");
return Ok(()); return Ok(());
} }
Message::Close(None) => { Message::Close(None) => {
log::debug!("host closed connection"); // log::debug!("host closed connection");
return Ok(()); return Ok(());
} }
}; };

View File

@ -7,6 +7,7 @@ use super::{HostComms, player::PlayerIdComms};
pub struct LobbyComms { pub struct LobbyComms {
comms: Comms, comms: Comms,
// TODO: move this to not use a receiver
connect_recv: Receiver<(PlayerId, bool)>, connect_recv: Receiver<(PlayerId, bool)>,
} }
@ -34,13 +35,17 @@ impl LobbyComms {
pub async fn next_message(&mut self) -> Result<Message, GameError> { pub async fn next_message(&mut self) -> Result<Message, GameError> {
tokio::select! { tokio::select! {
r = self.comms.message() => { r = self.comms.message() => {
r match r {
Ok(val) => Ok(val),
Err(GameError::GenericError(err)) => Err(GameError::GenericError(format!("comms message: {err}"))),
Err(err) => Err(err),
}
} }
r = self.connect_recv.recv() => { r = self.connect_recv.recv() => {
match r { match r {
Ok((player_id, true)) => Ok(Message::Connect(player_id)), Ok((player_id, true)) => Ok(Message::Connect(player_id)),
Ok((player_id, false)) => Ok(Message::Disconnect(player_id)), Ok((player_id, false)) => Ok(Message::Disconnect(player_id)),
Err(err) => Err(GameError::GenericError(err.to_string())), Err(err) => Err(GameError::GenericError(format!("connect recv: {err}"))),
} }
} }
} }

View File

@ -1,3 +1,5 @@
@use 'sass:color';
$wolves_color: rgba(255, 0, 0, 0.7); $wolves_color: rgba(255, 0, 0, 0.7);
$village_color: rgba(0, 0, 255, 0.7); $village_color: rgba(0, 0, 255, 0.7);
$connected_color: hsl(120, 68%, 50%); $connected_color: hsl(120, 68%, 50%);
@ -94,6 +96,8 @@ nav.debug-nav {
background-color: black; background-color: black;
color: #cccccc; color: #cccccc;
cursor: pointer; cursor: pointer;
width: fit-content;
text-align: center;
&:hover { &:hover {
background-color: white; background-color: white;
@ -104,9 +108,6 @@ nav.debug-nav {
.player { .player {
margin: 0px; margin: 0px;
// padding-left: 5px;
// padding-right: 5px;
// padding-bottom: 5px;
min-width: 10rem; min-width: 10rem;
max-width: 10vw; max-width: 10vw;
max-height: 4rem; max-height: 4rem;
@ -116,7 +117,6 @@ nav.debug-nav {
font-family: 'Cute Font'; font-family: 'Cute Font';
&.marked { &.marked {
// background-color: brighten($village_color, 100%);
filter: hue-rotate(90deg); filter: hue-rotate(90deg);
} }
@ -219,18 +219,17 @@ button {
background-color: #000; background-color: #000;
&:disabled { &:disabled {
filter: grayscale(80%); background-color: rgba(128, 128, 128, 0.5);
} color: rgb(128, 128, 128);
cursor: not-allowed;
&:disabled:hover {
filter: sepia(100%);
} }
&:disabled:hover::after { &:disabled:hover::after {
content: attr(reason); content: attr(reason);
position: absolute; position: absolute;
margin-top: 10px; margin-top: 10px;
top: 90%; // top: 90%;
// left: 0;
font: 'Cute Font'; font: 'Cute Font';
// color: #000; // color: #000;
// background-color: #fff; // background-color: #fff;
@ -258,11 +257,21 @@ button {
} }
.wolves-list { .wolves-intro {
@extend .column-list;
align-content: center;
width: 100%;
.wolves-list {
flex-wrap: wrap; flex-wrap: wrap;
flex-direction: row; flex-direction: row;
justify-content: space-evenly; justify-content: space-evenly;
flex: 1 1 0; flex: 1 1 0;
}
& button {
align-self: center;
}
} }
.character { .character {
@ -391,9 +400,12 @@ bool_role {
} }
.error-container { .error-container {
width: 70vw; position: fixed;
margin-left: 10vw; top: 10vh;
margin-right: 10vw; width: 100vw;
display: flex;
flex-direction: row;
align-content: center;
} }
.error-container button { .error-container button {
@ -412,14 +424,15 @@ bool_role {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
width: 100%; width: 80%;
margin: 30px; margin: 30px;
text-align: center; text-align: center;
// gap: 20px; // gap: 20px;
justify-content: center; justify-content: center;
gap: 30px; gap: 30px;
background-color: $error_color; background-color: $error_color;
filter: $error_filter; border: 1px solid color.change($error_color, $alpha: 1.0);
backdrop-filter: grayscale(100%);
padding-left: 5vw; padding-left: 5vw;
@ -507,9 +520,17 @@ clients {
} }
.role-reveal-card { .role-reveal-card {
border: 3px solid rgba(0, 0, 0, 0.5); min-width: 5cm;
background-color: rgba(255, 0, 0, 0.7); display: flex;
align-items: center;
align-content: center;
flex-direction: column;
gap: 10px;
padding: 10px;
border: 1px solid $wolves_color;
background-color: color.change($wolves_color, $alpha: 0.1);
min-width: 100px; min-width: 100px;
color: white;
& p.number { & p.number {
font-size: 2rem; font-size: 2rem;
@ -519,12 +540,22 @@ clients {
text-align: center; text-align: center;
} }
} &>button {
border: 1px solid $wolves_color;
$bg: color.change($wolves_color, $alpha: 0.2);
background-color: $bg;
.role-reveal-card.ready { &:hover {
background-color: rgba(0, 255, 0, 0.7); background-color: white;
} color: color.change($wolves_color, $alpha: 1.0);
}
}
&.ready {
border: 1px solid $village_color;
background-color: color.change($village_color, $alpha: 0.2);
}
}
.pronouns { .pronouns {
font-size: 70%; font-size: 70%;
@ -541,6 +572,8 @@ clients {
font-size: 2rem; font-size: 2rem;
justify-content: center; justify-content: center;
align-content: center;
align-items: center;
&.margin-20 { &.margin-20 {
margin-left: 20px; margin-left: 20px;
@ -562,6 +595,8 @@ clients {
.column-list { .column-list {
list-style: none; list-style: none;
justify-content: center; justify-content: center;
align-content: center;
align-items: center;
font-family: 'Cute Font'; font-family: 'Cute Font';
@ -607,6 +642,12 @@ clients {
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
text-align: center; text-align: center;
& button {
width: fit-content;
text-align: center;
align-self: center;
}
} }
.small { .small {
@ -767,11 +808,6 @@ input {
} }
} }
.zoom {
zoom: 200%;
}
.game-start-role { .game-start-role {
@extend .column-list; @extend .column-list;
text-align: center; text-align: center;
@ -826,3 +862,41 @@ input {
} }
} }
.character-picker {
display: flex;
flex-direction: column;
width: 100%;
align-items: center;
color: white;
$marked_bg: color.change($wolves_color, $alpha: 0.3);
$marked_border: color.change($wolves_color, $alpha: 1.0);
$village_bg: color.change($village_color, $alpha: 0.3);
$village_border: color.change($village_color, $alpha: 1.0);
.character {
padding: 0.5cm;
& * {
font-size: 1.5rem;
}
&.marked {
background-color: $marked_bg;
border: 1px solid $marked_border;
&:hover {
color: white;
background-color: $marked_border;
}
}
background-color: $village_bg;
border: 1px solid $village_border;
&:hover {
color: white;
background-color: $village_border;
}
}
}

View File

@ -391,9 +391,8 @@ impl Component for Host {
self.send.clone(), self.send.clone(),
); );
html! { html! {
<>
<h2>{format!("Day {}", day.get())}</h2>
<DaytimePlayerList <DaytimePlayerList
day={day}
marked={marked_for_execution} marked={marked_for_execution}
big_screen={self.big_screen} big_screen={self.big_screen}
characters={ characters={
@ -402,7 +401,6 @@ impl Component for Host {
on_execute={on_execute} on_execute={on_execute}
on_mark={on_mark} on_mark={on_mark}
/> />
</>
} }
} }
HostState::RoleReveal { ackd, waiting } => { HostState::RoleReveal { ackd, waiting } => {
@ -582,15 +580,16 @@ impl Component for Host {
HostEvent::SetBigScreenState(state) => { HostEvent::SetBigScreenState(state) => {
self.big_screen = state; self.big_screen = state;
if self.big_screen if self.big_screen
&& let Ok(Some(root)) = gloo::utils::document().query_selector("app") && let Some(root) = gloo::utils::document().document_element()
&& let Err(err) = root.set_attribute("style", "zoom: 200%;") && let Err(err) = root.set_attribute("style", "font-size: 3rem;")
{ {
log::error!("setting zoom: {err:?}"); log::error!("setting zoom: {err:?}");
} }
if state { if state {
let (discard_send, mut discard_recv) = futures::channel::mpsc::channel(10); let (mut discard_send, mut discard_recv) = futures::channel::mpsc::channel(10);
self.send = discard_send; core::mem::swap(&mut discard_send, &mut self.send);
Box::leak(Box::new(discard_send));
yew::platform::spawn_local(async move { yew::platform::spawn_local(async move {
while discard_recv.next().await.is_some() {} while discard_recv.next().await.is_some() {}
}); });

View File

@ -290,26 +290,24 @@ impl Component for SingleTarget {
.then(|| html!(<h2>{headline}</h2>)); .then(|| html!(<h2>{headline}</h2>));
let submit = target_selection.as_ref().map(|target_selection| { let submit = target_selection.as_ref().map(|target_selection| {
let disabled = self.selected.is_none(); let disabled = self.selected.is_none().then_some("pick a target");
let target_selection = target_selection.clone(); let target_selection = target_selection.clone();
let on_click = self let on_click = self
.selected .selected
.clone() .clone()
.map(|t| move |_| target_selection.emit(t.clone())); .map(|t| Callback::from(move |_| target_selection.emit(t.clone())))
.unwrap_or_default();
html! { html! {
<div class="button-container sp-ace"> <div class="button-container sp-ace">
<button <Button disabled_reason={disabled} on_click={on_click}>
disabled={disabled}
onclick={on_click}
>
{"submit"} {"submit"}
</button> </Button>
</div> </div>
} }
}); });
html! { html! {
<div class="column-list"> <div class="character-picker">
{headline} {headline}
{children.clone()} {children.clone()}
<div class="row-list"> <div class="row-list">
@ -344,81 +342,15 @@ pub struct TargetCardProps {
#[function_component] #[function_component]
fn TargetCard(props: &TargetCardProps) -> Html { fn TargetCard(props: &TargetCardProps) -> Html {
let submenu = {
let button_text = if props.selected { "unpick" } else { "pick" };
let character_id = props.target.character_id.clone(); let character_id = props.target.character_id.clone();
let on_select = props.on_select.clone(); let on_select = props.on_select.clone();
let on_click = Callback::from(move |_| on_select.emit(character_id.clone())); let on_click = Callback::from(move |_| on_select.emit(character_id.clone()));
html! {
<nav class="submenu">
<Button on_click={on_click}>{button_text}</Button>
</nav>
}
};
let marked = props.selected.then_some("marked"); let marked = props.selected.then_some("marked");
let ident: PublicIdentity = props.target.clone().into();
html! { html! {
<div class={"row-list baseline margin-5"}> <Button on_click={on_click} classes={classes!(marked, "character")}>
<div class={classes!("player", "ident", "column-list", marked)}> <Identity ident={ident}/>
<Identity ident={Into::<PublicIdentity>::into(&props.target)} /> </Button>
{submenu}
</div>
</div>
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct CustomTargetCardProps {
pub target: CharacterIdentity,
pub options: Arc<[String]>,
pub on_select: Option<Callback<(CharacterId, String)>>,
#[prop_or_default]
pub class: String,
#[prop_or(true)]
pub hide_submenu: bool,
}
#[function_component]
pub fn CustomTargetCard(
CustomTargetCardProps {
target,
options,
on_select,
class,
hide_submenu,
}: &CustomTargetCardProps,
) -> Html {
let submenu = options.is_empty().not().then(|| {
let buttons = options
.iter()
.cloned()
.map(|option| {
let on_select = on_select.clone();
let button_text = option.clone();
let character_id = target.character_id.clone();
let on_click = on_select
.map(|on_select| {
Callback::from(move |_| {
on_select.emit((character_id.clone(), option.clone()))
})
})
.unwrap_or_default();
html! {
<Button on_click={on_click}>{button_text}</Button>
}
})
.collect::<Html>();
html! {
<nav class={classes!("submenu", hide_submenu.not().then_some("shown"))}>
{buttons}
</nav>
}
});
html! {
<div class={"row-list baseline"}>
<div class={classes!("ident", "column-list", class)}>
<Identity ident={Into::<PublicIdentity>::into(target)} />
{submenu}
</div>
</div>
} }
} }

View File

@ -20,7 +20,7 @@ pub fn WolvesIntro(props: &WolvesIntroProps) -> Html {
let on_complete = props.on_complete.clone(); let on_complete = props.on_complete.clone();
let on_complete = Callback::from(move |_| on_complete.emit(())); let on_complete = Callback::from(move |_| on_complete.emit(()));
html! { html! {
<div class="column-list"> <div class="wolves-intro">
<h2>{"these are the wolves:"}</h2> <h2>{"these are the wolves:"}</h2>
<div class="row-list wolves-list"> <div class="row-list wolves-list">
{ {

View File

@ -7,6 +7,8 @@ pub struct ButtonProperties {
pub disabled_reason: Option<String>, pub disabled_reason: Option<String>,
#[prop_or_default] #[prop_or_default]
pub children: Html, pub children: Html,
#[prop_or_default]
pub classes: yew::Classes,
} }
#[function_component] #[function_component]
@ -15,7 +17,7 @@ pub fn Button(props: &ButtonProperties) -> Html {
let on_click = Callback::from(move |_| on_click.emit(())); let on_click = Callback::from(move |_| on_click.emit(()));
html! { html! {
<button <button
class="default-button" class={classes!("default-button", props.classes.clone())}
disabled={props.disabled_reason.is_some()} disabled={props.disabled_reason.is_some()}
reason={props.disabled_reason.clone()} reason={props.disabled_reason.clone()}
onclick={on_click} onclick={on_click}

View File

@ -1,4 +1,4 @@
use core::ops::Not; use core::{num::NonZeroU8, ops::Not};
use werewolves_proto::{ use werewolves_proto::{
message::{CharacterState, PublicIdentity}, message::{CharacterState, PublicIdentity},
@ -6,10 +6,11 @@ use werewolves_proto::{
}; };
use yew::prelude::*; use yew::prelude::*;
use crate::components::{Button, Identity}; use crate::components::{Button, ClickableField, Identity};
#[derive(Debug, Clone, PartialEq, Properties)] #[derive(Debug, Clone, PartialEq, Properties)]
pub struct DaytimePlayerListProps { pub struct DaytimePlayerListProps {
pub day: NonZeroU8,
pub characters: Box<[CharacterState]>, pub characters: Box<[CharacterState]>,
pub marked: Box<[CharacterId]>, pub marked: Box<[CharacterId]>,
pub on_execute: Callback<()>, pub on_execute: Callback<()>,
@ -20,6 +21,7 @@ pub struct DaytimePlayerListProps {
#[function_component] #[function_component]
pub fn DaytimePlayerList( pub fn DaytimePlayerList(
DaytimePlayerListProps { DaytimePlayerListProps {
day,
characters, characters,
on_execute, on_execute,
on_mark, on_mark,
@ -42,22 +44,22 @@ pub fn DaytimePlayerList(
} }
}) })
.collect::<Html>(); .collect::<Html>();
let button_text = if marked.is_empty() {
"end day"
} else {
"execute"
};
let button = big_screen.not().then(|| { let button = big_screen.not().then(|| {
html! { html! {
<Button <Button on_click={on_execute}>
on_click={on_execute} {button_text}
disabled_reason={
marked.is_empty()
.then_some(String::from("no one is on the block"))
}
>
{"execute"}
</Button> </Button>
} }
}); });
html! { html! {
<div class="column-list"> <div class="character-picker">
<div class="row-list small baseline player-list gap"> <h2>{"day "}{day.to_string()}</h2>
<div class="player-list">
{chars} {chars}
</div> </div>
{button} {button}
@ -87,26 +89,16 @@ pub fn DaytimePlayer(
}: &DaytimePlayerProps, }: &DaytimePlayerProps,
) -> Html { ) -> Html {
let dead = died_to.is_some().then_some("dead"); let dead = died_to.is_some().then_some("dead");
let button_text = if *on_the_block { "unmark" } else { "mark" }; let marked = on_the_block.then_some("marked");
let on_the_block = on_the_block.then_some("marked");
let submenu = died_to.is_none().then_some(()).and_then(|_| {
on_select.as_ref().map(|on_select| {
let character_id = identity.character_id.clone(); let character_id = identity.character_id.clone();
let on_select = on_select.clone(); let on_click: Callback<_> = on_select
let on_click = Callback::from(move |_| on_select.emit(character_id.clone())); .clone()
html! { .map(|on_select| Callback::from(move |_| on_select.emit(character_id.clone())))
<nav class="submenu"> .unwrap_or_default();
<Button on_click={on_click}>{button_text}</Button>
</nav>
}
})
});
let identity: PublicIdentity = identity.into(); let identity: PublicIdentity = identity.into();
html! { html! {
<div class={classes!("player", dead, on_the_block, "column-list", "ident")}> <Button on_click={on_click} classes={classes!(marked, dead, "character")}>
<Identity ident={identity}/> <Identity ident={identity}/>
{submenu} </Button>
</div>
} }
} }

View File

@ -67,7 +67,6 @@ pub fn LobbyPlayer(LobbyPlayerProps { player, on_action }: &LobbyPlayerProps) ->
}); });
html! { html! {
// <div class={classes!("player", class, "column-list")}>
<ClickableField <ClickableField
state={open} state={open}
options={submenu} options={submenu}
@ -76,7 +75,5 @@ pub fn LobbyPlayer(LobbyPlayerProps { player, on_action }: &LobbyPlayerProps) ->
> >
<Identity ident={player.identification.public.clone()}/> <Identity ident={player.identification.public.clone()}/>
</ClickableField> </ClickableField>
// {submenu}
// </div>
} }
} }

View File

@ -1,9 +1,9 @@
use std::sync::Arc; use core::ops::Not;
use werewolves_proto::message::CharacterIdentity; use werewolves_proto::message::{CharacterIdentity, PublicIdentity};
use yew::prelude::*; use yew::prelude::*;
use crate::components::{Button, action::CustomTargetCard}; use crate::components::{Button, Identity};
#[derive(Debug, PartialEq, Properties)] #[derive(Debug, PartialEq, Properties)]
pub struct RoleRevealProps { pub struct RoleRevealProps {
@ -65,12 +65,10 @@ impl Component for RoleReveal {
} }
}); });
html! { html! {
<div class="column-list gap"> <div class="role-reveal-cards">
{force_all} {force_all}
<div class={"role-reveal-cards"}>
{cards} {cards}
</div> </div>
</div>
} }
} }
} }
@ -87,25 +85,20 @@ pub fn RoleRevealCard(props: &RoleRevealCardProps) -> Html {
let class = props.is_ready.then_some("ready"); let class = props.is_ready.then_some("ready");
let target = props.target.clone(); let target = props.target.clone();
let on_force_ready = props.on_force_ready.clone(); let on_force_ready = props.on_force_ready.clone();
let on_click = on_force_ready.map(|on_force_ready| { let on_click = props.is_ready.not().then_some(()).and_then(|_| {
Callback::from(move |_| { on_force_ready.map(|on_force_ready| {
let on_click = Callback::from(move |_| {
on_force_ready.emit(target.clone()); on_force_ready.emit(target.clone());
});
html! {<Button on_click={on_click}>{"ready"}</Button>}
}) })
}); });
let options: Arc<[String]> = if props.is_ready || props.on_force_ready.is_none() {
Arc::new([]) let ident: PublicIdentity = props.target.clone().into();
} else {
Arc::new([String::from("ready")])
};
html! { html! {
<div class={classes!(class, "role-reveal-card")}> <div class={classes!(class, "role-reveal-card")}>
<CustomTargetCard <Identity ident={ident}/>
target={props.target.clone()} {on_click}
options={options}
class={if props.is_ready { String::from("ready") } else { String::new() }}
on_select={on_click}
hide_submenu=false
/>
</div> </div>
} }
} }

View File

@ -18,6 +18,8 @@ mod pages {
} }
mod callback; mod callback;
use core::num::NonZeroU8;
use pages::{ErrorComponent, WerewolfError}; use pages::{ErrorComponent, WerewolfError};
use web_sys::Url; use web_sys::Url;
use werewolves_proto::{ use werewolves_proto::{
@ -53,19 +55,14 @@ fn main() {
} }
} else if path.starts_with("/many-client") { } else if path.starts_with("/many-client") {
let clients = document.query_selector("clients").unwrap().unwrap(); let clients = document.query_selector("clients").unwrap().unwrap();
for (player_id, name, dupe) in [( for (player_id, name, num, dupe) in (1..=16).map(|num| {
PlayerId::from_u128(1),
"player 1".to_string(),
document.query_selector("app").unwrap().unwrap(),
)]
.into_iter()
.chain((1..=2).map(|num| {
( (
PlayerId::from_u128(num as u128), PlayerId::from_u128(num as u128),
format!("player {num}"), format!("player {num}"),
NonZeroU8::new(num).unwrap(),
document.create_element("autoclient").unwrap(), document.create_element("autoclient").unwrap(),
) )
})) { }) {
if dupe.tag_name() == "AUTOCLIENT" { if dupe.tag_name() == "AUTOCLIENT" {
clients.append_child(&dupe).unwrap(); clients.append_child(&dupe).unwrap();
} }
@ -80,7 +77,7 @@ fn main() {
public: PublicIdentity { public: PublicIdentity {
name: name.to_string(), name: name.to_string(),
pronouns: Some(String::from("he/him")), pronouns: Some(String::from("he/him")),
number: None, number: Some(num),
}, },
}), }),
}, },