werewolves/werewolves/src/components/settings.rs

651 lines
23 KiB
Rust

use core::{num::NonZeroU8, ops::Not};
use std::rc::Rc;
use convert_case::{Case, Casing};
use werewolves_proto::{
error::GameError,
game::{GameSettings, OrRandom, SetupRole, SetupSlot, SlotId},
message::{Identification, PlayerState, PublicIdentity},
role::RoleTitle,
};
use yew::prelude::*;
use crate::components::{
Button, ClickableField, Icon, IconSource, IconType, Identity, PartialAssociatedIcon,
client::Signin,
};
#[derive(Debug, PartialEq, Properties)]
pub struct SettingsProps {
pub settings: GameSettings,
pub players_in_lobby: Rc<[PlayerState]>,
pub on_update: Callback<GameSettings>,
pub on_start: Callback<()>,
pub on_add_player: Callback<PublicIdentity>,
pub qr_mode_button: Html,
}
#[function_component]
pub fn Settings(
SettingsProps {
settings,
players_in_lobby,
on_update,
on_start,
on_add_player,
qr_mode_button,
}: &SettingsProps,
) -> Html {
let players = players_in_lobby
.iter()
.map(|p| p.identification.clone())
.collect::<Rc<[_]>>();
let disabled_reason = settings
.check_with_player_list(&players)
.err()
.map(|err| err.to_string());
let roles_in_setup: Rc<[RoleTitle]> = settings
.slots()
.iter()
.map(|s| Into::<RoleTitle>::into(s.role.clone()))
.collect();
let on_update_role = on_update.clone();
let settings = Rc::new(settings.clone());
let on_update_role_settings = settings.clone();
let already_assigned_pids = settings
.slots()
.iter()
.filter_map(|r| r.assign_to)
.collect::<Box<[_]>>();
let players_for_assign = players
.iter()
.filter(|p| !already_assigned_pids.contains(&p.player_id))
.cloned()
.collect::<Rc<[_]>>();
let update_role_card = Callback::from(move |act: SettingSlotAction| {
let mut new_settings = (*on_update_role_settings).clone();
match act {
SettingSlotAction::Remove(slot_id) => new_settings.remove_slot(slot_id),
SettingSlotAction::Update(slot) => new_settings.update_slot(slot),
}
on_update_role.emit(new_settings);
});
let roles = settings
.slots()
.iter()
.map(|slot| {
html! {
<SettingsSlot
all_players={players.clone()}
players_for_assign={players_for_assign.clone()}
roles_in_setup={roles_in_setup.clone()}
slot={slot.clone()}
update={update_role_card.clone()}
/>
}
})
.collect::<Html>();
let add_roles_update = on_update.clone();
let sorted_role_tiles = {
let mut v = RoleTitle::ALL.to_vec();
v.sort_by_key(|v| Into::<SetupRole>::into(*v).category());
v
};
let add_roles_buttons = sorted_role_tiles
.into_iter()
.map(|r| {
let update = add_roles_update.clone();
let settings = settings.clone();
let on_click = Callback::from(move |_| {
let mut settings = (*settings).clone();
settings.new_slot(r);
update.emit(settings);
});
let class = Into::<SetupRole>::into(r).category().class();
let name = r.to_string().to_case(Case::Title);
// let icon = r.icon().map(|icon| {
// html! {
// <Icon source={icon} icon_type={IconType::Small}/>
// }
// });
html! {
<button
onclick={on_click}
class={classes!(class, "add-role")}
>
<span>{name}</span>
</button>
}
})
.collect::<Html>();
let clear_bad_assigned = matches!(
settings.check_with_player_list(&players),
Err(GameError::AssignedMultipleTimes(_, _)) | Err(GameError::AssignedPlayerMissing(_))
)
.then(|| {
let clear_settings = settings.clone();
let clear_update = on_update.clone();
let clear_players = players.clone();
let clear_bad_assigned = Callback::from(move |_| {
let mut settings = (*clear_settings).clone();
settings.remove_assignments_not_in_list(&clear_players);
settings.remove_duplicate_assignments();
clear_update.emit(settings);
});
html! {
<Button on_click={clear_bad_assigned}>
{"clear bad assignments"}
</Button>
}
});
let clear_all_assignments = settings
.slots()
.iter()
.any(|s| s.assign_to.is_some())
.then(|| {
let settings = settings.clone();
let update = on_update.clone();
let on_click = Callback::from(move |_| {
let mut settings = (*settings).clone();
settings
.slots()
.iter()
.filter(|s| s.assign_to.is_some())
.map(|s| {
let mut s = s.clone();
s.assign_to.take();
s
})
.collect::<Box<[_]>>()
.into_iter()
.for_each(|s| settings.update_slot(s));
update.emit(settings);
});
html! {
<Button on_click={on_click}>{"clear all assignments"}</Button>
}
});
let assignments = settings
.slots()
.iter()
.any(|s| s.assign_to.is_some())
.then(|| {
let assignments =
settings
.slots()
.iter()
.cloned()
.filter_map(|s| {
s.assign_to
.as_ref()
.map(|a| {
players
.iter()
.find(|p| p.player_id == *a)
.map(|assign| {
let class = s.role.category().class();
(html! {
<Identity ident={assign.public.clone()}/>
}, Some(class))
})
.unwrap_or_else(|| {
(html! {
<span>{"[left the lobby]"}</span>
}, None)
})
})
.map(|(who, class)| {
let assignments_update = on_update.clone();
let assignments_settings = settings.clone();
let click_slot = s.clone();
let on_click = Callback::from(move |_| {
let mut settings = (*assignments_settings).clone();
let mut click_slot = click_slot.clone();
click_slot.assign_to.take();
settings.update_slot(click_slot);
assignments_update.emit(settings);
});
html! {
<Button classes={classes!("assignment", class)} on_click={on_click}>
<label>{s.role.to_string().to_case(Case::Title)}</label>
{who}
</Button>
}
})
})
.collect::<Html>();
html! {
<>
<label>{"assignments"}</label>
<div class="assignments">
{assignments}
</div>
</>
}
});
let clear_setup = {
let update = on_update.clone();
let on_click = Callback::from(move |_| {
update.emit(GameSettings::empty());
});
let disabled_reason = settings.slots().is_empty().then_some("no setup to clear");
html! {
<Button on_click={on_click} disabled_reason={disabled_reason}>
{"clear setup"}
</Button>
}
};
let fill_empty_with_villagers = {
let disabled_reason =
(settings.min_players_needed() >= players.len()).then_some("no empty slots");
let update = on_update.clone();
let settings = settings.clone();
let player_count = players.len();
let on_click = Callback::from(move |_| {
let mut settings = (*settings).clone();
settings.fill_remaining_slots_with_villagers(player_count);
update.emit(settings);
});
html! {
<Button on_click={on_click} disabled_reason={disabled_reason}>
{"fill empty slots with villagers"}
</Button>
}
};
let add_player_open = use_state(|| false);
let add_player_opts = html! {
<div class="add-player">
<Signin callback={on_add_player.clone()}/>
</div>
};
html! {
<div class="settings">
<div class="top-settings">
{qr_mode_button.clone()}
{fill_empty_with_villagers}
{clear_setup}
{clear_all_assignments}
{clear_bad_assigned}
</div>
<ClickableField
options={add_player_opts}
state={add_player_open}
>
{"add player"}
</ClickableField>
<p>{format!("min roles for setup: {}", settings.min_players_needed())}</p>
<p>{format!("current role count: {}", settings.slots().len())}</p>
<div class="roles-add-list">
{add_roles_buttons}
</div>
<div class="roles-in-setup">
<h3>{"roles in the game"}</h3>
<div class="role-list">
{roles}
</div>
</div>
{assignments}
<Button
disabled_reason={disabled_reason}
classes={classes!("start-game")}
on_click={on_start.clone()}
>
{"start game"}
</Button>
</div>
}
}
pub enum SettingSlotAction {
Remove(SlotId),
Update(SetupSlot),
}
#[derive(Debug, PartialEq, Properties)]
pub struct SettingsSlotProps {
pub players_for_assign: Rc<[Identification]>,
pub roles_in_setup: Rc<[RoleTitle]>,
pub slot: SetupSlot,
pub update: Callback<SettingSlotAction>,
pub all_players: Rc<[Identification]>,
}
#[function_component]
pub fn SettingsSlot(
SettingsSlotProps {
players_for_assign,
roles_in_setup,
slot,
update,
all_players,
}: &SettingsSlotProps,
) -> Html {
let open = use_state(|| false);
let open_update = open.setter();
let update = update.clone();
let update = Callback::from(move |act| {
update.emit(act);
});
let role_name = slot.role.to_string().to_case(Case::Title);
let apprentice_open = use_state(|| false);
let submenu = {
let on_kick_update = update.clone();
let slot_id = slot.slot_id;
let assign_open = use_state(|| false);
let on_kick = Callback::from(move |_| {
on_kick_update.emit(SettingSlotAction::Remove(slot_id));
open_update.set(false);
});
let assign_to = assign_to_submenu(players_for_assign, slot, &update, &open.setter());
let options =
setup_options_for_slot(slot, &update, roles_in_setup, apprentice_open, open.clone());
let assign_text = slot
.assign_to
.as_ref()
.and_then(|assign_to| all_players.iter().find(|p| p.player_id == *assign_to))
.map(|assign_to| {
html! {
<>
<span>{"assigned to"}</span>
<Identity ident={assign_to.public.clone()} />
</>
}
})
.unwrap_or_else(|| html! {{"assign"}});
html! {
<>
<Button on_click={on_kick}>
{"remove"}
</Button>
<ClickableField
options={assign_to}
state={assign_open}
class={classes!("assign-list")}
>
{assign_text}
</ClickableField>
{options}
</>
}
};
let class = slot.role.category().class();
html! {
<ClickableField
class={classes!("setup-slot")}
button_class={classes!(class)}
options={submenu}
state={open}
with_backdrop_exit=true
>
<label>{role_name}</label>
</ClickableField>
}
}
fn assign_to_submenu(
players: &[Identification],
slot: &SetupSlot,
update: &Callback<SettingSlotAction>,
assign_setter: &UseStateSetter<bool>,
) -> Html {
let buttons = players
.iter()
.map(|p| {
let slot = slot.clone();
let update = update.clone();
let pid = p.player_id;
let setter = assign_setter.clone();
let on_click = Callback::from(move |_| {
let mut slot = slot.clone();
slot.assign_to.replace(pid);
update.emit(SettingSlotAction::Update(slot));
setter.set(false);
});
html! {
<Button on_click={on_click}>
<Identity ident={p.public.clone()}/>
</Button>
}
})
.collect::<Html>();
html! {
<div class="assignees">
{buttons}
</div>
}
}
fn setup_options_for_slot(
slot: &SetupSlot,
update: &Callback<SettingSlotAction>,
roles_in_setup: &[RoleTitle],
open_apprentice_assign: UseStateHandle<bool>,
slot_field_open: UseStateHandle<bool>,
) -> Html {
let setup_options_for_role = match &slot.role {
SetupRole::MasonLeader { recruits_available } => {
let next = {
let mut s = slot.clone();
match &mut s.role {
SetupRole::MasonLeader { recruits_available } => {
*recruits_available =
NonZeroU8::new(recruits_available.get().checked_add(1).unwrap_or(1))
.unwrap()
}
_ => unreachable!(),
}
s
};
let prev = recruits_available
.get()
.checked_sub(1)
.and_then(NonZeroU8::new)
.map(|new_avail| {
let mut s = slot.clone();
match &mut s.role {
SetupRole::MasonLeader { recruits_available } => {
*recruits_available = new_avail
}
_ => unreachable!(),
}
s
});
let increment_update = update.clone();
let on_increment = Callback::from(move |_| {
increment_update.emit(SettingSlotAction::Update(next.clone()))
});
let decrement_update = update.clone();
let on_decrement = prev
.clone()
.map(|prev| {
Callback::from(move |_| {
decrement_update.emit(SettingSlotAction::Update(prev.clone()))
})
})
.unwrap_or_default();
let decrement_disabled_reason = prev.is_none().then_some("at minimum");
Some(html! {
<>
<label>{"recruits"}</label>
<div class={classes!("increment-decrement")}>
<Button on_click={on_decrement} disabled_reason={decrement_disabled_reason}>{"-"}</Button>
<label>{recruits_available.get().to_string()}</label>
<Button on_click={on_increment}>{"+"}</Button>
</div>
</>
})
}
SetupRole::Scapegoat { redeemed } => {
let next = {
let next_redeemed = match redeemed {
OrRandom::Random => OrRandom::Determined(true),
OrRandom::Determined(true) => OrRandom::Determined(false),
OrRandom::Determined(false) => OrRandom::Random,
};
let mut s = slot.clone();
match &mut s.role {
SetupRole::Scapegoat { redeemed } => *redeemed = next_redeemed,
_ => unreachable!(),
}
s
};
let update = update.clone();
let on_click =
Callback::from(move |_| update.emit(SettingSlotAction::Update(next.clone())));
let body = match redeemed {
OrRandom::Determined(true) => "redeemed",
OrRandom::Determined(false) => "irredeemable",
OrRandom::Random => "random",
};
Some(html! {
<>
<label>{"redeemed?"}</label>
<Button on_click={on_click}>
<label>{body}</label>
</Button>
</>
})
}
SetupRole::Apprentice { to: specifically } => {
let options = roles_in_setup
.iter()
.filter(|r| r.is_mentor())
.cloned()
.collect::<Box<[_]>>();
#[allow(clippy::obfuscated_if_else)]
options
.is_empty()
.not()
.then(|| {
options
.into_iter()
.filter(|o| specifically.as_ref().map(|s| s != o).unwrap_or(true))
.map(|option| {
let open_apprentice_assign = open_apprentice_assign.clone();
let open = slot_field_open.clone();
let update = update.clone();
let role_name = option.to_string().to_case(Case::Title);
let mut slot = slot.clone();
match &mut slot.role {
SetupRole::Apprentice { to: specifically } => {
specifically.replace(option);
}
_ => unreachable!(),
}
let on_click = Callback::from(move |_| {
update.emit(SettingSlotAction::Update(slot.clone()));
open_apprentice_assign.set(false);
open.set(false);
});
html! {
<Button on_click={on_click}>
{role_name}
</Button>
}
})
.chain(specifically.is_some().then(|| {
let open_apprentice_assign = open_apprentice_assign.clone();
let open = slot_field_open.clone();
let update = update.clone();
let role_name = "random";
let mut slot = slot.clone();
match &mut slot.role {
SetupRole::Apprentice { to: specifically } => {
specifically.take();
}
_ => unreachable!(),
}
let on_click = Callback::from(move |_| {
update.emit(SettingSlotAction::Update(slot.clone()));
open_apprentice_assign.set(false);
open.set(false);
});
html! {
<Button on_click={on_click}>
{role_name}
</Button>
}
}))
.collect::<Html>()
})
.map(|options| {
let current = specifically
.as_ref()
.map(|r| r.to_string().to_case(Case::Title))
.unwrap_or_else(|| String::from("random"));
html! {
<ClickableField
state={open_apprentice_assign}
options={options}
>
{"mentor ("}{current}{")"}
</ClickableField>
}
})
}
SetupRole::Elder { knows_on_night } => {
const SAFE_NUM: NonZeroU8 = NonZeroU8::new(1).unwrap();
let increment_slot = {
let mut s = slot.clone();
match &mut s.role {
SetupRole::Elder { knows_on_night } => {
*knows_on_night =
NonZeroU8::new(knows_on_night.get().checked_add(1).unwrap_or(1))
.unwrap_or(SAFE_NUM)
}
_ => unreachable!(),
}
s
};
let decrement_slot = {
let mut s = slot.clone();
match &mut s.role {
SetupRole::Elder { knows_on_night } => {
*knows_on_night =
NonZeroU8::new(knows_on_night.get().checked_sub(1).unwrap_or(u8::MAX))
.unwrap_or(SAFE_NUM)
}
_ => unreachable!(),
}
s
};
let update_decrement = update.clone();
let decrement = Callback::from(move |_| {
update_decrement.emit(SettingSlotAction::Update(decrement_slot.clone()))
});
let update_increment = update.clone();
let increment = Callback::from(move |_| {
update_increment.emit(SettingSlotAction::Update(increment_slot.clone()))
});
Some(html! {
<>
<label>{"knows on night"}</label>
<div class={classes!("increment-decrement")}>
<Button on_click={decrement}>{"-"}</Button>
<label>{knows_on_night.to_string()}</label>
<Button on_click={increment}>{"+"}</Button>
</div>
</>
})
}
_ => None,
};
setup_options_for_role.unwrap_or_default()
}