player self seat number update

This commit is contained in:
emilis 2026-02-17 22:05:13 +00:00
parent 6fb105ca82
commit ab1cb29952
No known key found for this signature in database
10 changed files with 307 additions and 55 deletions

View File

@ -592,6 +592,7 @@ dialog .tab-content {
} }
.player-lobby { .player-lobby {
user-select: none;
margin: 5vh 15vw 5vh 15vw; margin: 5vh 15vw 5vh 15vw;
padding: 3ch; padding: 3ch;
font-size: 1.5em; font-size: 1.5em;
@ -604,6 +605,74 @@ dialog .tab-content {
} }
} }
.player-additional {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.number-info {
display: flex;
flex-direction: column;
border: 1px solid rgb(0, 64, 0);
background-color: rgba(0, 64, 0, 0.3);
gap: 0.25ch;
width: fit-content;
padding: 3ch;
min-width: 40vw;
align-items: center;
&.unset {
border: 1px solid rgb(128, 0, 0);
background-color: rgba(64, 0, 0, 0.3);
}
#player-number {
text-align: center;
}
.number-text {
font-size: 1.5em;
user-select: none;
.number {
font-size: 1.25em;
}
&.unset {
font-size: 1.25em;
color: rgb(192, 0, 0);
font-weight: bold;
}
}
}
.number-update:not([hidden]) {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
gap: 0.25ch;
margin-top: 3ch;
font-size: 1.5em;
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
display: none;
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
appearance: textfield;
}
input[type=submit] {
@extend button;
}
}
.tutorial-box { .tutorial-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -19,7 +19,7 @@ use werewolves_macros::Titles;
use crate::{character::CharacterId, game::GameTime}; use crate::{character::CharacterId, game::GameTime};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Titles)]
pub enum DiedTo { pub enum DiedTo {
Execution { Execution {
day: NonZeroU8, day: NonZeroU8,

View File

@ -69,6 +69,7 @@ pub enum ServerToClientMessage {
LobbyInfo { LobbyInfo {
joined: bool, joined: bool,
players: Box<[PublicIdentity]>, players: Box<[PublicIdentity]>,
current_number: Option<NonZeroU8>,
}, },
GameInProgress, GameInProgress,
GameStart { GameStart {

View File

@ -197,14 +197,14 @@ impl DeadChat {
} }
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeadChatMessage { pub struct DeadChatMessage {
pub id: Uuid, pub id: Uuid,
pub timestamp: DateTime<Utc>, pub timestamp: DateTime<Utc>,
pub message: DeadChatContent, pub message: DeadChatContent,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeadChatContent { pub enum DeadChatContent {
PlayerMessage { PlayerMessage {
from: CharacterIdentity, from: CharacterIdentity,

View File

@ -31,7 +31,7 @@ pub struct PublicIdentity {
pub number: Option<NonZeroU8>, pub number: Option<NonZeroU8>,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CharacterIdentity { pub struct CharacterIdentity {
pub character_id: CharacterId, pub character_id: CharacterId,
pub name: String, pub name: String,

View File

@ -6,24 +6,20 @@ use leptos_router::hooks::use_url;
use reactive_stores::Store; use reactive_stores::Store;
use crate::app::{ use crate::app::{
components::LinkButton, components::{DialogModal, LinkButton},
storage::user::{AuthContext, AuthContextStoreFields}, storage::user::{AuthContext, AuthContextStoreFields},
}; };
#[server] #[server]
pub async fn get_active_game(token: TokenString) -> Result<Option<GameId>, ServerError> { pub async fn get_active_game(token: TokenString) -> Result<Option<GameId>, ServerError> {
let db = use_context::<api::state::AppState>() let db = expect_context::<api::state::AppState>().db;
.expect("no app state")
.db;
let user = db.user().check_token(&token).await?; let user = db.user().check_token(&token).await?;
Ok(db.game().get_joined_active_game(user.id).await?) Ok(db.game().get_joined_active_game(user.id).await?)
} }
#[server] #[server]
pub async fn new_game(token: TokenString) -> Result<GameId, ServerError> { pub async fn new_game(token: TokenString) -> Result<GameId, ServerError> {
let db = use_context::<api::state::AppState>() let db = expect_context::<api::state::AppState>().db;
.expect("no app state")
.db;
let user = db.user().check_token(&token).await?; let user = db.user().check_token(&token).await?;
Ok(db.game().new_game(user.id).await?.id) Ok(db.game().new_game(user.id).await?.id)
} }
@ -100,6 +96,7 @@ pub fn Nav() -> impl IntoView {
} }
}) })
}; };
view! { view! {
{home_button} {home_button}
<span class="display-name">{session.name().to_string()}</span> <span class="display-name">{session.name().to_string()}</span>

View File

@ -1,10 +1,21 @@
werewolves_macros::include_path!("werewolves/src/app/pages/game/player"); werewolves_macros::include_path!("werewolves/src/app/pages/game/player");
use core::{
hash::Hash,
num::{self, NonZeroU8},
ops::{Deref, Not},
};
use std::collections::HashSet;
use convert_case::{Case, Casing}; use convert_case::{Case, Casing};
use leptos::{ev::MouseEvent, prelude::*}; use leptos::{
ev::{Event, MouseEvent, SubmitEvent, Targeted},
prelude::*,
};
use werewolves_proto::{ use werewolves_proto::{
message::{ message::{
ClientDeadChat, ClientMessage, ServerToClientMessage as Srv2Client, dead::DeadChatMessage, ClientDeadChat, ClientMessage, ServerToClientMessage as Srv2Client, UpdateSelf,
dead::DeadChatMessage,
}, },
role::RoleTitle, role::RoleTitle,
}; };
@ -13,8 +24,13 @@ use crate::{ConsoleLogError, app::components::ErrorBox};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Page { enum Page {
Lobby { joined: bool }, Lobby {
RoleReveal { role: RoleTitle }, joined: bool,
current_number: Option<NonZeroU8>,
},
RoleReveal {
role: RoleTitle,
},
GameInProgress, GameInProgress,
Sleep, Sleep,
DeadChat, DeadChat,
@ -27,7 +43,7 @@ pub fn PlayerGamePage(
disconnect: RwSignal<bool>, disconnect: RwSignal<bool>,
) -> impl IntoView { ) -> impl IntoView {
let error = RwSignal::new(None); let error = RwSignal::new(None);
let dead_chat: RwSignal<Option<Vec<DeadChatMessage>>> = RwSignal::new(None); let dead_chat: RwSignal<Option<HashSet<HashedDeadChatMessage>>> = RwSignal::new(None);
let page: RwSignal<Option<Page>> = RwSignal::new(None); let page: RwSignal<Option<Page>> = RwSignal::new(None);
Effect::new(move || { Effect::new(move || {
let Some(message) = message.get() else { let Some(message) = message.get() else {
@ -35,18 +51,30 @@ pub fn PlayerGamePage(
}; };
match message { match message {
Srv2Client::Disconnect => disconnect.set(true), Srv2Client::Disconnect => disconnect.set(true),
Srv2Client::LobbyInfo { joined, .. } => page.set(Some(Page::Lobby { joined })), Srv2Client::LobbyInfo {
joined,
current_number,
..
} => page.set(Some(Page::Lobby {
joined,
current_number,
})),
Srv2Client::GameInProgress => page.set(Some(Page::GameInProgress)), Srv2Client::GameInProgress => page.set(Some(Page::GameInProgress)),
Srv2Client::GameStart { role } => page.set(Some(Page::RoleReveal { role })), Srv2Client::GameStart { role } => page.set(Some(Page::RoleReveal { role })),
Srv2Client::Story(game_story) => todo!("{game_story:#?}"), Srv2Client::Story(game_story) => todo!("{game_story:#?}"),
Srv2Client::Update(_) => {} Srv2Client::Update(_) => {}
Srv2Client::DeadChat(dead_chat_messages) => { Srv2Client::DeadChat(dead_chat_messages) => {
dead_chat.set(Some(dead_chat_messages.to_vec())); dead_chat.set(Some(
dead_chat_messages
.into_iter()
.map(Into::<HashedDeadChatMessage>::into)
.collect(),
));
page.set(Some(Page::DeadChat)); page.set(Some(Page::DeadChat));
} }
Srv2Client::DeadChatMessage(dead_chat_message) => { Srv2Client::DeadChatMessage(dead_chat_message) => {
if let Some(chat) = dead_chat.write().as_mut() { if let Some(chat) = dead_chat.write().as_mut() {
chat.push(dead_chat_message); chat.insert(dead_chat_message.into());
page.set(Some(Page::DeadChat)); page.set(Some(Page::DeadChat));
} else { } else {
reply.set(Some(ClientMessage::DeadChat(ClientDeadChat::GetHistory))); reply.set(Some(ClientMessage::DeadChat(ClientDeadChat::GetHistory)));
@ -61,6 +89,8 @@ pub fn PlayerGamePage(
} }
// match message {} // match message {}
}); });
let joined = RwSignal::new(false);
let current_number = RwSignal::new(None);
let content = move || { let content = move || {
let Some(page) = page.get() else { let Some(page) = page.get() else {
return ().into_any(); return ().into_any();
@ -68,32 +98,19 @@ pub fn PlayerGamePage(
match page { match page {
Page::DeadChat => todo!("dead chat"), Page::DeadChat => todo!("dead chat"),
Page::Sleep => view! { <h1>"go to sleep"</h1> }.into_any(), Page::Sleep => view! { <h1>"go to sleep"</h1> }.into_any(),
Page::Lobby { joined } => { Page::Lobby {
let click = move |ev: MouseEvent| { joined: j,
ev.prevent_default(); current_number: c,
reply.set(Some(if joined { } => {
ClientMessage::Goodbye joined.set(j);
} else { current_number.set(c);
ClientMessage::Hello
}));
};
let text = match joined {
true => view! {
<h2>"you are in the lobby"</h2>
<p>"you're all good c:"</p>
}
.into_any(),
false => view! {
<h2>"you are not in the lobby"</h2>
<p>"join if you want to play"</p>
}
.into_any(),
};
view! { view! {
<div class="player-lobby" class:joined=joined> <PlayerLobby
{text} reply=reply
<button on:click=click>{if joined { "leave" } else { "join" }}</button> joined=joined.read_only()
</div> current_number=current_number.read_only()
error=error
/>
} }
.into_any() .into_any()
} }
@ -123,3 +140,39 @@ pub fn PlayerGamePage(
} }
.into_any() .into_any()
} }
#[repr(transparent)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HashedDeadChatMessage(DeadChatMessage);
impl Ord for HashedDeadChatMessage {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.timestamp.cmp(&other.timestamp)
}
}
impl PartialOrd for HashedDeadChatMessage {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Hash for HashedDeadChatMessage {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl From<DeadChatMessage> for HashedDeadChatMessage {
fn from(value: DeadChatMessage) -> Self {
Self(value)
}
}
impl From<HashedDeadChatMessage> for DeadChatMessage {
fn from(value: HashedDeadChatMessage) -> Self {
value.0
}
}
impl Deref for HashedDeadChatMessage {
type Target = DeadChatMessage;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View File

@ -0,0 +1,128 @@
use core::num::NonZeroU8;
use leptos::{
ev::{Event, MouseEvent, SubmitEvent, Targeted},
prelude::*,
};
use werewolves_proto::message::{ClientMessage, UpdateSelf};
#[component]
pub fn PlayerLobby(
reply: WriteSignal<Option<ClientMessage>>,
joined: ReadSignal<bool>,
current_number: ReadSignal<Option<NonZeroU8>>,
error: RwSignal<Option<String>>,
) -> impl IntoView {
let click = move |ev: MouseEvent| {
ev.prevent_default();
reply.set(Some(if joined.get() {
ClientMessage::Goodbye
} else {
ClientMessage::Hello
}));
};
let text = move || match joined.get() {
true => view! {
<h2>"you are in the lobby"</h2>
<p>"you're all good c:"</p>
}
.into_any(),
false => view! {
<h2>"you are not in the lobby"</h2>
<p>"join if you want to play"</p>
}
.into_any(),
};
let number: RwSignal<Option<NonZeroU8>> = RwSignal::new(None);
let number = move || {
let form_hidden = RwSignal::new(true);
let number_update = {
let update = move |e: Targeted<Event, leptos::web_sys::HtmlInputElement>| {
let value = e.target().value();
if value.trim().is_empty() {
number.set(None);
return;
}
let current = number.get_untracked();
let default = current.map(|c| c.get().to_string()).unwrap_or_default();
let value_u8 = match e.target().value().trim().parse::<u8>() {
Ok(v) => v,
Err(_) => {
e.target().set_value(default.as_str());
return;
}
};
if let Some(nz) = NonZeroU8::new(value_u8) {
number.set(Some(nz));
} else {
e.target().set_value(default.as_str());
return;
}
};
let submit = move |ev: SubmitEvent| {
ev.prevent_default();
let Some(num) = number.get() else {
error.set(Some("please set a number".into()));
return;
};
reply.set(Some(ClientMessage::UpdateSelf(UpdateSelf::Number(num))));
form_hidden.set(true);
};
view! {
<form class="number-update" on:submit=submit hidden=move || form_hidden.get()>
<label for="player-number">"change seat number"</label>
<input
id="player-number"
type="number"
autocomplete="off"
on:input:target=update
value=move || {
number.get().map(|n| n.get().to_string()).unwrap_or_default()
}
/>
<input value="submit" type="submit" />
</form>
}
.into_any()
};
match current_number.get() {
Some(num) => {
form_hidden.set(true);
view! {
<span class="number-text">
"you are in seat "<span class="number">{num.get()}</span>
</span>
<button
on:click=move |_| form_hidden.set(false)
hidden=move || !form_hidden.get()
style="width: fit-content; font-size: 1.5em;"
>
"change seat"
</button>
{number_update}
}
.into_any()
}
None => {
form_hidden.set(false);
view! {
<span class="number-text unset">"you don't have a seat number :c"</span>
<span class="number-text">"please put your seat number in below"</span>
{number_update}
}
.into_any()
}
}
};
view! {
<div class="player-lobby" class:joined=joined>
{text}
<button on:click=click>{move || if joined.get() { "leave" } else { "join" }}</button>
</div>
<div class="player-additional">
<div class="number-info" class:unset=move || current_number.get().is_none()>
{number}
</div>
</div>
}
}

View File

@ -24,9 +24,7 @@ pub fn UserSettings() -> impl IntoView {
match tut_read.read().enabled { match tut_read.read().enabled {
true => view! { <button on:click=move |_| tut_write.write().enabled = false>"disable tutorials"</button> } true => view! { <button on:click=move |_| tut_write.write().enabled = false>"disable tutorials"</button> }
.into_any(), .into_any(),
false => view! { false => view! { <button on:click=move |_| tut_write.write().enabled = true>"enable tutorials"</button> }.into_any(),
<button on:click=move |_| tut_write.write().enabled = true>"enable tutorials"</button>
}.into_any(),
} }
}; };
view! { view! {

View File

@ -69,19 +69,20 @@ impl<'a> Lobby<'a> {
} }
pub async fn send_lobby_info_to_clients(&mut self) -> Result<(), ServerError> { pub async fn send_lobby_info_to_clients(&mut self) -> Result<(), ServerError> {
let (players, identities) = self let joined = self.db.game().get_joined_players(self.game_id).await?;
.db let (players, identities) = joined
.game() .iter()
.get_joined_players(self.game_id) .map(|p| (p.player_id, p.public.clone()))
.await?
.into_iter()
.map(|p| (p.player_id, p.public))
.collect::<(Vec<_>, Vec<_>)>(); .collect::<(Vec<_>, Vec<_>)>();
let identities = identities.into_boxed_slice(); let identities = identities.into_boxed_slice();
super::send_to_all_players_in_game_filtered(self.game_id, move |pid| { super::send_to_all_players_in_game_filtered(self.game_id, move |pid| {
Some(ServerToClientMessage::LobbyInfo { Some(ServerToClientMessage::LobbyInfo {
joined: players.contains(&pid), joined: players.contains(&pid),
players: identities.clone(), players: identities.clone(),
current_number: joined
.iter()
.find_map(|j| (j.player_id == pid).then_some(j.public.number))
.flatten(),
}) })
}) })
.await; .await;
@ -257,11 +258,16 @@ impl<'a> Lobby<'a> {
self.send_lobby_info_to_clients().await.log_debug(loc!()); self.send_lobby_info_to_clients().await.log_debug(loc!());
} }
HostOrClientMessage::Client(IdentifiedClientMessage { HostOrClientMessage::Client(IdentifiedClientMessage {
identity: Identification { player_id, .. }, identity:
Identification {
player_id,
public: PublicIdentity { number, .. },
},
update: ClientUpdate::Message(ClientMessage::GetState), update: ClientUpdate::Message(ClientMessage::GetState),
}) => { }) => {
let joined = self.db.game().get_joined_players(self.game_id).await?; let joined = self.db.game().get_joined_players(self.game_id).await?;
let msg = ServerToClientMessage::LobbyInfo { let msg = ServerToClientMessage::LobbyInfo {
current_number: number,
joined: joined.iter().any(|p| p.player_id == player_id), joined: joined.iter().any(|p| p.player_id == player_id),
players: joined.into_iter().map(|p| p.public).collect(), players: joined.into_iter().map(|p| p.public).collect(),
}; };