werewolves/werewolves/src/app/pages/game/host/settings.rs

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> }
}