host: change seat number for players
This commit is contained in:
parent
852973eddf
commit
14e8f369ea
|
|
@ -3064,6 +3064,12 @@ dependencies = [
|
|||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sorted-vec"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f58d7b0190c7f12df7e8be6b79767a0836059159811b869d5ab55721fe14d0"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
|
|
@ -4053,6 +4059,7 @@ dependencies = [
|
|||
"rand 0.9.2",
|
||||
"reactive_stores",
|
||||
"serde",
|
||||
"sorted-vec",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ ciborium = { version = "0.2" }
|
|||
pretty_assertions = { version = "1.4" }
|
||||
colored = { version = "3.1" }
|
||||
pretty_env_logger = { version = "0.5" }
|
||||
sorted-vec = { version = "0.8" }
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 0
|
||||
|
|
|
|||
|
|
@ -241,6 +241,10 @@ dialog::backdrop {
|
|||
&.full {
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
|
||||
.headline {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -281,26 +285,7 @@ dialog::backdrop {
|
|||
max-width: 70%;
|
||||
}
|
||||
|
||||
form {
|
||||
font-size: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1ch;
|
||||
align-items: center;
|
||||
|
||||
.form-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
// width: 100%;
|
||||
gap: 1ch;
|
||||
}
|
||||
|
||||
|
||||
label {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-settings-list {
|
||||
|
|
@ -659,13 +644,24 @@ dialog .tab-content {
|
|||
}
|
||||
}
|
||||
|
||||
.host-seat-changer {
|
||||
width: min-content;
|
||||
|
||||
input[type=submit] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.number-update:not([hidden]) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.25ch;
|
||||
margin-top: 3ch;
|
||||
|
||||
.bigger {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
|
|
@ -756,3 +752,35 @@ dialog .tab-content {
|
|||
border: 1px solid red;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.player-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
max-width: 80vw;
|
||||
gap: 0.25ch;
|
||||
min-width: 45vw;
|
||||
|
||||
&>* {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
font-size: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25ch;
|
||||
align-items: center;
|
||||
|
||||
.form-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1ch;
|
||||
}
|
||||
|
||||
label {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use core::hash::Hash;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
|
|
@ -204,6 +205,24 @@ pub struct DeadChatMessage {
|
|||
pub message: DeadChatContent,
|
||||
}
|
||||
|
||||
impl Hash for DeadChatMessage {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for DeadChatMessage {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.id.cmp(&other.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for DeadChatMessage {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DeadChatContent {
|
||||
PlayerMessage {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ werewolves-macros.workspace = true
|
|||
werewolves-proto.workspace = true
|
||||
codee.workspace = true
|
||||
convert_case.workspace = true
|
||||
sorted-vec.workspace = true
|
||||
|
||||
[features]
|
||||
hydrate = [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
use core::num::NonZeroU8;
|
||||
|
||||
use leptos::{
|
||||
ev::{Event, MouseEvent, SubmitEvent, Targeted},
|
||||
html::Form,
|
||||
prelude::*,
|
||||
tachys::html::node_ref::NodeRefContainer,
|
||||
web_sys::HtmlInputElement,
|
||||
};
|
||||
|
||||
use crate::ConsoleLogError;
|
||||
|
||||
#[component]
|
||||
pub fn ChangePlayerNumber(
|
||||
submitted_number: WriteSignal<Option<NonZeroU8>>,
|
||||
error: WriteSignal<Option<String>>,
|
||||
) -> impl IntoView {
|
||||
let number: RwSignal<Option<NonZeroU8>> = RwSignal::new(None);
|
||||
let update = move |e: Targeted<Event, HtmlInputElement>| {
|
||||
e.prevent_default();
|
||||
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(err) => {
|
||||
log::error!("{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());
|
||||
}
|
||||
};
|
||||
let submit = move |ev: SubmitEvent| {
|
||||
ev.prevent_default();
|
||||
log::warn!("called submit with number: {:?}", number.get());
|
||||
let Some(num) = number.get() else {
|
||||
error.set(Some("please set a number".into()));
|
||||
return;
|
||||
};
|
||||
submitted_number.set(Some(num));
|
||||
};
|
||||
let submit_click = move |ev: MouseEvent| {
|
||||
ev.prevent_default();
|
||||
log::warn!("called submit with number: {:?}", number.get());
|
||||
let Some(num) = number.get() else {
|
||||
error.set(Some("please set a number".into()));
|
||||
return;
|
||||
};
|
||||
submitted_number.set(Some(num));
|
||||
};
|
||||
move || {
|
||||
view! {
|
||||
<form class="number-update" on:submit=submit>
|
||||
<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" on:click=submit_click/>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ pub enum DialogMode {
|
|||
|
||||
#[component]
|
||||
pub fn DialogModal(
|
||||
text: String,
|
||||
button_content: impl IntoView + Clone,
|
||||
#[prop(optional)] open: Option<RwSignal<bool>>,
|
||||
#[prop(optional)] button_class: String,
|
||||
#[prop(default = Box::new(|| ().into_any()))] mut children: ChildrenFnMut,
|
||||
|
|
@ -23,15 +23,13 @@ pub fn DialogModal(
|
|||
// use generated id if not supplied
|
||||
let open = open.unwrap_or_else(|| RwSignal::new(false));
|
||||
|
||||
let close_cb = {
|
||||
move |ev: MouseEvent| {
|
||||
let close_cb = move |ev: MouseEvent| {
|
||||
ev.prevent_default();
|
||||
open.set(false);
|
||||
}
|
||||
};
|
||||
let close = {
|
||||
view! {
|
||||
<button on:click=close_cb.clone() class="close">
|
||||
<button on:click=close_cb class="close">
|
||||
"close"
|
||||
</button>
|
||||
}
|
||||
|
|
@ -48,9 +46,7 @@ pub fn DialogModal(
|
|||
.into_any(),
|
||||
};
|
||||
let dialog_element: NodeRef<leptos::html::Dialog> = NodeRef::new();
|
||||
let on_backdrop_click = {
|
||||
let close_cb = close_cb.clone();
|
||||
move |ev: MouseEvent| {
|
||||
let on_backdrop_click = move |ev: MouseEvent| {
|
||||
ev.prevent_default();
|
||||
if !close_backdrop {
|
||||
return;
|
||||
|
|
@ -77,7 +73,6 @@ pub fn DialogModal(
|
|||
close_cb(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let is_open = move || open.get();
|
||||
let modal = view! {
|
||||
|
|
@ -97,11 +92,13 @@ pub fn DialogModal(
|
|||
ev.prevent_default();
|
||||
open.set(true);
|
||||
};
|
||||
let button = text.is_empty().not().then_some(view! {
|
||||
<button class=button_class on:click=on_click>
|
||||
{text.clone()}
|
||||
let button = {
|
||||
view! {
|
||||
<button class=button_class.clone() on:click=on_click>
|
||||
{button_content.clone().into_view()}
|
||||
</button>
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
view! { <div class="dialog-modal">{button} {modal}</div> }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ pub fn HostGamePage(
|
|||
HostPage::None => ().into_any(),
|
||||
HostPage::Settings => view! {
|
||||
<Settings
|
||||
error=error
|
||||
open_categories=open_categories
|
||||
settings=settings.read_only()
|
||||
players=players.read_only()
|
||||
|
|
@ -122,7 +123,7 @@ fn CancelGame(
|
|||
<div hidden=move || derive_hidden.get()>
|
||||
<DialogModal
|
||||
button_class="cancel-game-button".into()
|
||||
text="cancel game".into()
|
||||
button_content="cancel game"
|
||||
open=open
|
||||
>
|
||||
<h1>"this will cancel the game. are you sure?"</h1>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,49 @@
|
|||
use leptos::prelude::*;
|
||||
use werewolves_proto::message::{Identification, PlayerState};
|
||||
use core::num::NonZeroU8;
|
||||
|
||||
use crate::app::components::{Equals, IdentificationInline, Sample, TutorialBox};
|
||||
use leptos::prelude::*;
|
||||
use werewolves_proto::message::{
|
||||
Identification, PlayerState,
|
||||
host::{HostLobbyMessage, HostMessage},
|
||||
};
|
||||
|
||||
use crate::app::{
|
||||
class::AsClasses,
|
||||
components::{
|
||||
DialogModal, DialogMode, Equals, IdentificationInline, Sample, TutorialBox,
|
||||
input::ChangePlayerNumber,
|
||||
},
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn HostPlayerList(players: ReadSignal<Box<[PlayerState]>>) -> impl IntoView {
|
||||
pub fn HostPlayerList(
|
||||
players: ReadSignal<Box<[PlayerState]>>,
|
||||
reply: WriteSignal<Option<HostMessage>>,
|
||||
error: WriteSignal<Option<String>>,
|
||||
) -> impl IntoView {
|
||||
let players = move || {
|
||||
let mut players = players.get();
|
||||
players.sort_by_key(|p| p.identification.public.number.unwrap_or(NonZeroU8::MAX));
|
||||
players
|
||||
.read()
|
||||
.iter()
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
let ident = p.identification.clone();
|
||||
let button_content = move || {
|
||||
view! { <IdentificationInline ident=ident.clone() /> }
|
||||
};
|
||||
let connected = if p.connected {
|
||||
"connected"
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
view! {
|
||||
<button class="player" class:connected=p.connected>
|
||||
<IdentificationInline ident=p.identification.clone() />
|
||||
</button>
|
||||
<DialogModal
|
||||
button_class=["player", connected].as_classes().to_string()
|
||||
button_content=button_content
|
||||
close_backdrop=true
|
||||
mode=DialogMode::Box
|
||||
>
|
||||
<HostPlayerDialogBody error=error player=p.clone() reply=reply />
|
||||
</DialogModal>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
|
|
@ -53,3 +83,46 @@ pub fn HostPlayerList(players: ReadSignal<Box<[PlayerState]>>) -> impl IntoView
|
|||
</TutorialBox>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn HostPlayerDialogBody(
|
||||
player: PlayerState,
|
||||
reply: WriteSignal<Option<HostMessage>>,
|
||||
error: WriteSignal<Option<String>>,
|
||||
) -> impl IntoView {
|
||||
let pid = player.identification.player_id;
|
||||
let kick = move |_| reply.set(Some(HostMessage::Lobby(HostLobbyMessage::Kick(pid))));
|
||||
let set_number = RwSignal::new(None);
|
||||
let num_change_open = RwSignal::new(false);
|
||||
let num_change_button_text = move || {
|
||||
if num_change_open.get() {
|
||||
"close seat changer"
|
||||
} else {
|
||||
"change seat number"
|
||||
}
|
||||
};
|
||||
Effect::new(move || {
|
||||
if let Some(num) = set_number.get() {
|
||||
log::info!("gunna set seat number to {num}");
|
||||
reply.set(Some(HostMessage::Lobby(HostLobbyMessage::SetPlayerNumber(
|
||||
pid, num,
|
||||
))));
|
||||
}
|
||||
});
|
||||
let toggle_change_seat_open = move |_| num_change_open.set(!num_change_open.get());
|
||||
move || {
|
||||
view! {
|
||||
<div class="headline">
|
||||
<IdentificationInline ident=player.identification.clone() />
|
||||
</div>
|
||||
|
||||
<div class="player-options">
|
||||
<button on:click=kick>"kick"</button>
|
||||
<button on:click=toggle_change_seat_open>{num_change_button_text}</button>
|
||||
<div class="host-seat-changer" hidden=move || !num_change_open.get()>
|
||||
<ChangePlayerNumber submitted_number=set_number.write_only() error=error />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ pub fn Settings(
|
|||
dialog_open: RwSignal<HashMap<SlotId, bool>>,
|
||||
open_categories: RwSignal<HashMap<Category, bool>>,
|
||||
reply: WriteSignal<Option<HostMessage>>,
|
||||
error: WriteSignal<Option<String>>,
|
||||
) -> impl IntoView {
|
||||
let slots = move || {
|
||||
settings
|
||||
|
|
@ -129,7 +130,7 @@ pub fn Settings(
|
|||
<div class="role-add-list">{categories}</div>
|
||||
<div class="setup-slots">{slots}</div>
|
||||
</div>
|
||||
<HostPlayerList players=players />
|
||||
<HostPlayerList error=error players=players reply=reply />
|
||||
}
|
||||
}
|
||||
#[component]
|
||||
|
|
@ -211,7 +212,7 @@ fn SettingsSetupSlot(
|
|||
]
|
||||
.as_classes()
|
||||
.to_string()
|
||||
text=setup_slot.read().role.title().to_string().to_case(Case::Title)
|
||||
button_content=setup_slot.read().role.title().to_string().to_case(Case::Title)
|
||||
close_backdrop=true
|
||||
>
|
||||
<SlotSettingsDialogBody setup_slot=setup_slot players=players />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use std::collections::HashSet;
|
|||
|
||||
use convert_case::{Case, Casing};
|
||||
use leptos::prelude::*;
|
||||
use sorted_vec::SortedSet;
|
||||
use werewolves_proto::{
|
||||
message::{
|
||||
ClientDeadChat, ClientMessage, ServerToClientMessage as Srv2Client, dead::DeadChatMessage,
|
||||
|
|
@ -35,8 +36,7 @@ pub fn PlayerGamePage(
|
|||
reply: WriteSignal<Option<ClientMessage>>,
|
||||
disconnect: RwSignal<bool>,
|
||||
) -> impl IntoView {
|
||||
let error = RwSignal::new(None);
|
||||
let dead_chat: RwSignal<Option<HashSet<HashedDeadChatMessage>>> = RwSignal::new(None);
|
||||
let dead_chat: RwSignal<Option<SortedSet<DeadChatMessage>>> = RwSignal::new(None);
|
||||
let page: RwSignal<Option<Page>> = RwSignal::new(None);
|
||||
Effect::new(move || {
|
||||
let Some(message) = message.get() else {
|
||||
|
|
@ -64,17 +64,12 @@ pub fn PlayerGamePage(
|
|||
Srv2Client::Story(game_story) => todo!("{game_story:#?}"),
|
||||
Srv2Client::Update(_) => {}
|
||||
Srv2Client::DeadChat(dead_chat_messages) => {
|
||||
dead_chat.set(Some(
|
||||
dead_chat_messages
|
||||
.into_iter()
|
||||
.map(Into::<HashedDeadChatMessage>::into)
|
||||
.collect(),
|
||||
));
|
||||
dead_chat.set(Some(dead_chat_messages.into_iter().collect()));
|
||||
page.set(Some(Page::DeadChat));
|
||||
}
|
||||
Srv2Client::DeadChatMessage(dead_chat_message) => {
|
||||
if let Some(chat) = dead_chat.write().as_mut() {
|
||||
chat.insert(dead_chat_message.into());
|
||||
chat.push(dead_chat_message);
|
||||
page.set(Some(Page::DeadChat));
|
||||
} else {
|
||||
reply.set(Some(ClientMessage::DeadChat(ClientDeadChat::GetHistory)));
|
||||
|
|
@ -134,45 +129,5 @@ pub fn PlayerGamePage(
|
|||
.into_any(),
|
||||
}
|
||||
};
|
||||
view! {
|
||||
<ErrorBox msg=error />
|
||||
{content}
|
||||
}
|
||||
.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! { {content} }.into_any()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ pub fn PlayerLobby(
|
|||
reply: WriteSignal<Option<ClientMessage>>,
|
||||
joined: ReadSignal<bool>,
|
||||
current_number: ReadSignal<Option<NonZeroU8>>,
|
||||
error: RwSignal<Option<String>>,
|
||||
error: WriteSignal<Option<String>>,
|
||||
) -> impl IntoView {
|
||||
let click = move |ev: MouseEvent| {
|
||||
ev.prevent_default();
|
||||
|
|
@ -68,7 +68,7 @@ pub fn PlayerLobby(
|
|||
form_hidden.set(true);
|
||||
};
|
||||
view! {
|
||||
<form class="number-update" on:submit=submit hidden=move || form_hidden.get()>
|
||||
<form class="number-update bigger" on:submit=submit hidden=move || form_hidden.get()>
|
||||
<label for="player-number">"change seat number"</label>
|
||||
<input
|
||||
id="player-number"
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ pub fn ChangePasswordButton() -> impl IntoView {
|
|||
};
|
||||
|
||||
view! {
|
||||
<DialogModal text="change password".into() close_backdrop=false>
|
||||
<DialogModal button_content="change password" close_backdrop=false>
|
||||
<ErrorBox msg=error />
|
||||
<form>
|
||||
<div class="form-fields">
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ pub fn UpdateProfileButton() -> impl IntoView {
|
|||
.then_some(view! { <p>"profile updated"</p> })
|
||||
};
|
||||
view! {
|
||||
<DialogModal text="update profile".into() close_backdrop=false>
|
||||
<DialogModal button_content="update profile" close_backdrop=false>
|
||||
<ErrorBox msg=error />
|
||||
<form>
|
||||
<label for="display-name">"display name"</label>
|
||||
|
|
|
|||
Loading…
Reference in New Issue