331 lines
11 KiB
Rust
331 lines
11 KiB
Rust
use core::ops::Not;
|
|
use std::{collections::HashMap, sync::Arc};
|
|
|
|
use convert_case::{Case, Casing};
|
|
use leptos::{ev::MouseEvent, prelude::*};
|
|
use werewolves_proto::{
|
|
aura::AuraTitle,
|
|
game::{Category, GameSettings, SetupSlot},
|
|
message::{PlayerState, host::HostMessage},
|
|
role::{Role, RoleTitle},
|
|
};
|
|
|
|
use crate::app::{
|
|
class::{AsClasses, Class, PartialClass},
|
|
components::{DialogModal, DialogMode, IdentityInline},
|
|
pages::game::host::HostPlayerList,
|
|
};
|
|
|
|
#[component]
|
|
pub fn Settings(
|
|
settings: RwSignal<GameSettings>,
|
|
players: ReadSignal<Box<[PlayerState]>>,
|
|
qr_mode: RwSignal<bool>,
|
|
dialog_open: RwSignal<bool>,
|
|
open_categories: RwSignal<HashMap<Category, bool>>,
|
|
) -> impl IntoView {
|
|
let slots = move || {
|
|
settings
|
|
.read()
|
|
.slots()
|
|
.iter()
|
|
.cloned()
|
|
.map(move |s| {
|
|
let signal = RwSignal::new(s);
|
|
Effect::watch(
|
|
move || signal.get(),
|
|
move |slot_update, _, _| settings.write().update_slot(slot_update.clone()),
|
|
false,
|
|
);
|
|
|
|
view! { <SettingsSetupSlot setup_slot=signal players=players dialog_open=dialog_open /> }
|
|
})
|
|
.collect_view()
|
|
};
|
|
let qr_mode_btn = move || {
|
|
let qr_toggle = move |ev: MouseEvent| {
|
|
ev.prevent_default();
|
|
qr_mode.set(!qr_mode.get());
|
|
};
|
|
match qr_mode.get() {
|
|
true => view! { <button on:click=qr_toggle>"disable qr mode"</button> }.into_any(),
|
|
false => view! { <button on:click=qr_toggle>"enable qr mode"</button> }.into_any(),
|
|
}
|
|
};
|
|
let roles_by_category = {
|
|
let mut r: HashMap<Category, Vec<RoleTitle>> = HashMap::new();
|
|
for role in RoleTitle::ALL {
|
|
if let Some(existing) = r.get_mut(&role.category()) {
|
|
existing.push(role);
|
|
} else {
|
|
r.insert(role.category(), vec![role]);
|
|
}
|
|
}
|
|
r
|
|
};
|
|
let ordered_keys = {
|
|
let mut k = roles_by_category.keys().copied().collect::<Box<_>>();
|
|
k.sort();
|
|
k
|
|
};
|
|
Effect::new(|| log::debug!("rendering settings"));
|
|
let categories = ordered_keys
|
|
.into_iter()
|
|
.map(|c| {
|
|
let roles_by_category = roles_by_category.clone();
|
|
let roles = move || {
|
|
open_categories
|
|
.with(|open| open.get(&c).copied().unwrap_or_default())
|
|
.then(|| {
|
|
roles_by_category
|
|
.get(&c)
|
|
.unwrap()
|
|
.iter()
|
|
.copied()
|
|
.map(|r| {
|
|
let add_role = move |ev: MouseEvent| {
|
|
ev.prevent_default();
|
|
settings.write().new_slot(r);
|
|
};
|
|
let classes =
|
|
["add-role", r.class(), "faint", "hover", "box"].as_classes();
|
|
view! {
|
|
<button class=classes on:click=add_role>
|
|
{r.to_string().to_case(Case::Title)}
|
|
</button>
|
|
}
|
|
})
|
|
.collect_view()
|
|
})
|
|
};
|
|
let toggle = move |ev: MouseEvent| {
|
|
ev.prevent_default();
|
|
let is_open = open_categories
|
|
.with_untracked(|open| open.get(&c).copied().unwrap_or_default());
|
|
open_categories.write().insert(c, !is_open);
|
|
};
|
|
let classes = ["title", c.class(), "hover", "box"].as_classes();
|
|
view! {
|
|
<div class="category">
|
|
<button class=classes on:click=toggle>
|
|
{c.to_string()}
|
|
</button>
|
|
<div class="roles">{roles}</div>
|
|
</div>
|
|
}
|
|
})
|
|
.collect_view();
|
|
|
|
view! {
|
|
<div class="game-settings">
|
|
<div class="top-bar">{qr_mode_btn}</div>
|
|
<div class="role-add-list">{categories}</div>
|
|
<div class="setup-slots">{slots}</div>
|
|
</div>
|
|
<HostPlayerList players=players />
|
|
}
|
|
}
|
|
#[component]
|
|
fn SettingsSetupSlot(
|
|
setup_slot: RwSignal<SetupSlot>,
|
|
players: ReadSignal<Box<[PlayerState]>>,
|
|
dialog_open: RwSignal<bool>,
|
|
) -> impl IntoView {
|
|
let auras = move || {
|
|
let slot = setup_slot.read();
|
|
slot.auras.is_empty().not().then(|| {
|
|
slot.auras
|
|
.iter()
|
|
.map(|a| {
|
|
view! { <span class="aura">{a.to_string().to_case(Case::Title)}</span> }
|
|
})
|
|
.collect_view()
|
|
})
|
|
};
|
|
let assigned_to = move || {
|
|
setup_slot.read().assign_to.map(|a| {
|
|
match players
|
|
.read()
|
|
.iter()
|
|
.find(|p| p.identification.player_id == a)
|
|
{
|
|
Some(player) => {
|
|
let ident = RwSignal::new(player.identification.public.clone());
|
|
view! {
|
|
<span class="assignment">
|
|
<IdentityInline ident=ident.read_only() />
|
|
</span>
|
|
}
|
|
.into_any()
|
|
}
|
|
None => {
|
|
view! { <span class="missing error">"missing player "{a.to_string()}</span> }
|
|
.into_any()
|
|
}
|
|
}
|
|
})
|
|
};
|
|
move || {
|
|
view! {
|
|
<div class="setup-slot-container">
|
|
<DialogModal
|
|
open=dialog_open
|
|
mode=DialogMode::Box
|
|
button_class=[
|
|
"setup-slot",
|
|
setup_slot.read().role.category().class(),
|
|
"faint",
|
|
"hover",
|
|
"box",
|
|
]
|
|
.as_classes()
|
|
.to_string()
|
|
text=setup_slot.read().role.title().to_string().to_case(Case::Title)
|
|
close_backdrop=true
|
|
>
|
|
<SlotSettingsDialogBody setup_slot=setup_slot players=players />
|
|
</DialogModal>
|
|
{assigned_to}
|
|
{auras}
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn SlotSettingsDialogBody(
|
|
setup_slot: RwSignal<SetupSlot>,
|
|
players: ReadSignal<Box<[PlayerState]>>,
|
|
) -> impl IntoView {
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
enum OpenTab {
|
|
#[default]
|
|
Auras,
|
|
PlayerAssignment,
|
|
}
|
|
let tab = RwSignal::new(OpenTab::default());
|
|
let tab_view = move || match tab.get() {
|
|
OpenTab::Auras => view! { <AuraSelection setup_slot=setup_slot /> }.into_any(),
|
|
OpenTab::PlayerAssignment => {
|
|
view! { <AssignmentSelection setup_slot=setup_slot players=players /> }.into_any()
|
|
}
|
|
};
|
|
move || {
|
|
let assigned_to = setup_slot
|
|
.read()
|
|
.assign_to
|
|
.as_ref()
|
|
.and_then(|pid| {
|
|
players
|
|
.read()
|
|
.iter()
|
|
.find(|p| p.identification.player_id == *pid)
|
|
.cloned()
|
|
})
|
|
.map(|p| p.identification.public)
|
|
.map(|id| view! { <IdentityInline ident=RwSignal::new(id).read_only() /> }.into_any())
|
|
.unwrap_or_else(|| view! { "none" }.into_any());
|
|
view! {
|
|
<span class=[
|
|
"role-title",
|
|
setup_slot.read().role.category().class(),
|
|
"underline",
|
|
"text-color",
|
|
]
|
|
.as_classes()>
|
|
{setup_slot.read().role.title().to_string().to_case(Case::Title)}
|
|
</span>
|
|
<div class="tabs">
|
|
<div class="tab">
|
|
<button
|
|
on:click=move |_| tab.set(OpenTab::Auras)
|
|
class:selected=move || matches!(*tab.read(), OpenTab::Auras)
|
|
>
|
|
"auras"
|
|
</button>
|
|
<span class="detail">"currently: "{setup_slot.read().auras.len()}</span>
|
|
</div>
|
|
<div class="tab">
|
|
<button
|
|
on:click=move |_| { tab.set(OpenTab::PlayerAssignment) }
|
|
class:selected=move || matches!(*tab.read(), OpenTab::PlayerAssignment)
|
|
>
|
|
"assignments"
|
|
</button>
|
|
<span class="detail">"currently: "{assigned_to}</span>
|
|
</div>
|
|
</div>
|
|
<div class="tab-content">{tab_view}</div>
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn AuraSelection(setup_slot: RwSignal<SetupSlot>) -> impl IntoView {
|
|
let auras = move || {
|
|
AuraTitle::ALL
|
|
.iter()
|
|
.copied()
|
|
.filter(|a| setup_slot.read().role.title().can_assign_aura(*a))
|
|
.map(|aura| {
|
|
let toggle = move |ev: MouseEvent| {
|
|
ev.prevent_default();
|
|
let mut slot = setup_slot.write();
|
|
if slot.auras.contains(&aura) {
|
|
slot.auras.retain(|a| aura != *a);
|
|
} else {
|
|
slot.auras.push(aura);
|
|
}
|
|
};
|
|
view! {
|
|
<button
|
|
class=["faint", "box", "hover", aura.partial_class().unwrap_or_default()]
|
|
.as_classes()
|
|
class:selected=setup_slot.read().auras.contains(&aura)
|
|
on:click=toggle
|
|
>
|
|
{aura.to_string().to_case(Case::Title)}
|
|
</button>
|
|
}
|
|
})
|
|
.collect_view()
|
|
};
|
|
|
|
view! { <div class="toggle-list">{auras}</div> }
|
|
}
|
|
|
|
#[component]
|
|
fn AssignmentSelection(
|
|
setup_slot: RwSignal<SetupSlot>,
|
|
players: ReadSignal<Box<[PlayerState]>>,
|
|
) -> impl IntoView {
|
|
let players = move || {
|
|
players
|
|
.read()
|
|
.iter()
|
|
.map(|p| {
|
|
let assigned = setup_slot
|
|
.read()
|
|
.assign_to
|
|
.as_ref()
|
|
.map(|assigned_id| p.identification.player_id == *assigned_id)
|
|
.unwrap_or_default();
|
|
let ident = RwSignal::new(p.identification.public.clone());
|
|
let pid = p.identification.player_id;
|
|
let assign = move |ev: MouseEvent| {
|
|
ev.prevent_default();
|
|
setup_slot.write().assign_to = assigned.not().then_some(pid);
|
|
};
|
|
|
|
view! {
|
|
<button on:click=assign class:selected=assigned class="player-select">
|
|
<IdentityInline ident=ident.read_only() />
|
|
</button>
|
|
}
|
|
})
|
|
.collect_view()
|
|
};
|
|
|
|
view! { <div class="toggle-list">{players}</div> }
|
|
}
|