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

View File

@ -7,6 +7,7 @@ use super::{HostComms, player::PlayerIdComms};
pub struct LobbyComms {
comms: Comms,
// TODO: move this to not use a receiver
connect_recv: Receiver<(PlayerId, bool)>,
}
@ -34,13 +35,17 @@ impl LobbyComms {
pub async fn next_message(&mut self) -> Result<Message, GameError> {
tokio::select! {
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() => {
match r {
Ok((player_id, true)) => Ok(Message::Connect(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);
$village_color: rgba(0, 0, 255, 0.7);
$connected_color: hsl(120, 68%, 50%);
@ -94,6 +96,8 @@ nav.debug-nav {
background-color: black;
color: #cccccc;
cursor: pointer;
width: fit-content;
text-align: center;
&:hover {
background-color: white;
@ -104,9 +108,6 @@ nav.debug-nav {
.player {
margin: 0px;
// padding-left: 5px;
// padding-right: 5px;
// padding-bottom: 5px;
min-width: 10rem;
max-width: 10vw;
max-height: 4rem;
@ -116,7 +117,6 @@ nav.debug-nav {
font-family: 'Cute Font';
&.marked {
// background-color: brighten($village_color, 100%);
filter: hue-rotate(90deg);
}
@ -219,18 +219,17 @@ button {
background-color: #000;
&:disabled {
filter: grayscale(80%);
}
&:disabled:hover {
filter: sepia(100%);
background-color: rgba(128, 128, 128, 0.5);
color: rgb(128, 128, 128);
cursor: not-allowed;
}
&:disabled:hover::after {
content: attr(reason);
position: absolute;
margin-top: 10px;
top: 90%;
// top: 90%;
// left: 0;
font: 'Cute Font';
// color: #000;
// 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-direction: row;
justify-content: space-evenly;
flex: 1 1 0;
}
& button {
align-self: center;
}
}
.character {
@ -391,9 +400,12 @@ bool_role {
}
.error-container {
width: 70vw;
margin-left: 10vw;
margin-right: 10vw;
position: fixed;
top: 10vh;
width: 100vw;
display: flex;
flex-direction: row;
align-content: center;
}
.error-container button {
@ -412,14 +424,15 @@ bool_role {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
width: 80%;
margin: 30px;
text-align: center;
// gap: 20px;
justify-content: center;
gap: 30px;
background-color: $error_color;
filter: $error_filter;
border: 1px solid color.change($error_color, $alpha: 1.0);
backdrop-filter: grayscale(100%);
padding-left: 5vw;
@ -507,9 +520,17 @@ clients {
}
.role-reveal-card {
border: 3px solid rgba(0, 0, 0, 0.5);
background-color: rgba(255, 0, 0, 0.7);
min-width: 5cm;
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;
color: white;
& p.number {
font-size: 2rem;
@ -519,12 +540,22 @@ clients {
text-align: center;
}
}
&>button {
border: 1px solid $wolves_color;
$bg: color.change($wolves_color, $alpha: 0.2);
background-color: $bg;
.role-reveal-card.ready {
background-color: rgba(0, 255, 0, 0.7);
}
&:hover {
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 {
font-size: 70%;
@ -541,6 +572,8 @@ clients {
font-size: 2rem;
justify-content: center;
align-content: center;
align-items: center;
&.margin-20 {
margin-left: 20px;
@ -562,6 +595,8 @@ clients {
.column-list {
list-style: none;
justify-content: center;
align-content: center;
align-items: center;
font-family: 'Cute Font';
@ -607,6 +642,12 @@ clients {
flex-direction: column;
justify-content: center;
text-align: center;
& button {
width: fit-content;
text-align: center;
align-self: center;
}
}
.small {
@ -767,11 +808,6 @@ input {
}
}
.zoom {
zoom: 200%;
}
.game-start-role {
@extend .column-list;
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(),
);
html! {
<>
<h2>{format!("Day {}", day.get())}</h2>
<DaytimePlayerList
day={day}
marked={marked_for_execution}
big_screen={self.big_screen}
characters={
@ -402,7 +401,6 @@ impl Component for Host {
on_execute={on_execute}
on_mark={on_mark}
/>
</>
}
}
HostState::RoleReveal { ackd, waiting } => {
@ -582,15 +580,16 @@ impl Component for Host {
HostEvent::SetBigScreenState(state) => {
self.big_screen = state;
if self.big_screen
&& let Ok(Some(root)) = gloo::utils::document().query_selector("app")
&& let Err(err) = root.set_attribute("style", "zoom: 200%;")
&& let Some(root) = gloo::utils::document().document_element()
&& let Err(err) = root.set_attribute("style", "font-size: 3rem;")
{
log::error!("setting zoom: {err:?}");
}
if state {
let (discard_send, mut discard_recv) = futures::channel::mpsc::channel(10);
self.send = discard_send;
let (mut discard_send, mut discard_recv) = futures::channel::mpsc::channel(10);
core::mem::swap(&mut discard_send, &mut self.send);
Box::leak(Box::new(discard_send));
yew::platform::spawn_local(async move {
while discard_recv.next().await.is_some() {}
});

View File

@ -290,26 +290,24 @@ impl Component for SingleTarget {
.then(|| html!(<h2>{headline}</h2>));
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 on_click = self
.selected
.clone()
.map(|t| move |_| target_selection.emit(t.clone()));
.map(|t| Callback::from(move |_| target_selection.emit(t.clone())))
.unwrap_or_default();
html! {
<div class="button-container sp-ace">
<button
disabled={disabled}
onclick={on_click}
>
<Button disabled_reason={disabled} on_click={on_click}>
{"submit"}
</button>
</Button>
</div>
}
});
html! {
<div class="column-list">
<div class="character-picker">
{headline}
{children.clone()}
<div class="row-list">
@ -344,81 +342,15 @@ pub struct TargetCardProps {
#[function_component]
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 on_select = props.on_select.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 ident: PublicIdentity = props.target.clone().into();
html! {
<div class={"row-list baseline margin-5"}>
<div class={classes!("player", "ident", "column-list", marked)}>
<Identity ident={Into::<PublicIdentity>::into(&props.target)} />
{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>
<Button on_click={on_click} classes={classes!(marked, "character")}>
<Identity ident={ident}/>
</Button>
}
}

View File

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

View File

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

View File

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

View File

@ -67,7 +67,6 @@ pub fn LobbyPlayer(LobbyPlayerProps { player, on_action }: &LobbyPlayerProps) ->
});
html! {
// <div class={classes!("player", class, "column-list")}>
<ClickableField
state={open}
options={submenu}
@ -76,7 +75,5 @@ pub fn LobbyPlayer(LobbyPlayerProps { player, on_action }: &LobbyPlayerProps) ->
>
<Identity ident={player.identification.public.clone()}/>
</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 crate::components::{Button, action::CustomTargetCard};
use crate::components::{Button, Identity};
#[derive(Debug, PartialEq, Properties)]
pub struct RoleRevealProps {
@ -65,12 +65,10 @@ impl Component for RoleReveal {
}
});
html! {
<div class="column-list gap">
<div class="role-reveal-cards">
{force_all}
<div class={"role-reveal-cards"}>
{cards}
</div>
</div>
}
}
}
@ -87,25 +85,20 @@ pub fn RoleRevealCard(props: &RoleRevealCardProps) -> Html {
let class = props.is_ready.then_some("ready");
let target = props.target.clone();
let on_force_ready = props.on_force_ready.clone();
let on_click = on_force_ready.map(|on_force_ready| {
Callback::from(move |_| {
let on_click = props.is_ready.not().then_some(()).and_then(|_| {
on_force_ready.map(|on_force_ready| {
let on_click = Callback::from(move |_| {
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([])
} else {
Arc::new([String::from("ready")])
};
let ident: PublicIdentity = props.target.clone().into();
html! {
<div class={classes!(class, "role-reveal-card")}>
<CustomTargetCard
target={props.target.clone()}
options={options}
class={if props.is_ready { String::from("ready") } else { String::new() }}
on_select={on_click}
hide_submenu=false
/>
<Identity ident={ident}/>
{on_click}
</div>
}
}

View File

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