2025-10-02 17:52:12 +01:00
|
|
|
use core::{num::NonZeroU8, ops::Not};
|
2025-10-05 10:54:47 +01:00
|
|
|
use std::rc::Rc;
|
2025-10-02 17:52:12 +01:00
|
|
|
|
2025-06-23 09:48:28 +01:00
|
|
|
use convert_case::{Case, Casing};
|
2025-10-02 17:52:12 +01:00
|
|
|
use werewolves_proto::{
|
2025-10-04 17:50:29 +01:00
|
|
|
error::GameError,
|
|
|
|
|
game::{GameSettings, OrRandom, SetupRole, SetupSlot, SlotId},
|
2025-10-10 18:34:59 +01:00
|
|
|
message::{Identification, PlayerState, PublicIdentity},
|
2025-10-04 17:50:29 +01:00
|
|
|
role::RoleTitle,
|
2025-10-02 17:52:12 +01:00
|
|
|
};
|
2025-06-23 09:48:28 +01:00
|
|
|
use yew::prelude::*;
|
|
|
|
|
|
2025-10-29 23:29:27 +00:00
|
|
|
use crate::components::{
|
|
|
|
|
Button, ClickableField, Icon, IconSource, IconType, Identity, PartialAssociatedIcon,
|
|
|
|
|
client::Signin,
|
|
|
|
|
};
|
2025-06-23 09:48:28 +01:00
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq, Properties)]
|
|
|
|
|
pub struct SettingsProps {
|
|
|
|
|
pub settings: GameSettings,
|
2025-10-02 17:52:12 +01:00
|
|
|
pub players_in_lobby: Rc<[PlayerState]>,
|
2025-06-23 09:48:28 +01:00
|
|
|
pub on_update: Callback<GameSettings>,
|
|
|
|
|
pub on_start: Callback<()>,
|
2025-10-10 18:34:59 +01:00
|
|
|
pub on_add_player: Callback<PublicIdentity>,
|
2025-10-07 01:47:59 +01:00
|
|
|
pub qr_mode_button: Html,
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-04 17:50:29 +01:00
|
|
|
#[function_component]
|
|
|
|
|
pub fn Settings(
|
|
|
|
|
SettingsProps {
|
|
|
|
|
settings,
|
|
|
|
|
players_in_lobby,
|
|
|
|
|
on_update,
|
|
|
|
|
on_start,
|
2025-10-10 18:34:59 +01:00
|
|
|
on_add_player,
|
2025-10-07 01:47:59 +01:00
|
|
|
qr_mode_button,
|
2025-10-04 17:50:29 +01:00
|
|
|
}: &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());
|
2025-06-23 09:48:28 +01:00
|
|
|
|
2025-10-04 17:50:29 +01:00
|
|
|
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()
|
2025-10-05 10:54:47 +01:00
|
|
|
.filter_map(|r| r.assign_to)
|
2025-10-04 17:50:29 +01:00
|
|
|
.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
|
2025-10-10 18:34:59 +01:00
|
|
|
all_players={players.clone()}
|
2025-10-04 17:50:29 +01:00
|
|
|
players_for_assign={players_for_assign.clone()}
|
|
|
|
|
roles_in_setup={roles_in_setup.clone()}
|
|
|
|
|
slot={slot.clone()}
|
|
|
|
|
update={update_role_card.clone()}
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect::<Html>();
|
2025-06-23 09:48:28 +01:00
|
|
|
|
2025-10-04 17:50:29 +01:00
|
|
|
let add_roles_update = on_update.clone();
|
2025-10-07 01:47:59 +01:00
|
|
|
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()
|
2025-10-04 17:50:29 +01:00
|
|
|
.map(|r| {
|
|
|
|
|
let update = add_roles_update.clone();
|
|
|
|
|
let settings = settings.clone();
|
|
|
|
|
let on_click = Callback::from(move |_| {
|
|
|
|
|
let mut settings = (*settings).clone();
|
2025-10-07 01:47:59 +01:00
|
|
|
settings.new_slot(r);
|
2025-10-04 17:50:29 +01:00
|
|
|
update.emit(settings);
|
|
|
|
|
});
|
2025-10-07 01:47:59 +01:00
|
|
|
let class = Into::<SetupRole>::into(r).category().class();
|
2025-10-04 17:50:29 +01:00
|
|
|
let name = r.to_string().to_case(Case::Title);
|
2025-10-29 23:29:27 +00:00
|
|
|
// let icon = r.icon().map(|icon| {
|
|
|
|
|
// html! {
|
|
|
|
|
// <Icon source={icon} icon_type={IconType::Small}/>
|
|
|
|
|
// }
|
|
|
|
|
// });
|
2025-10-04 17:50:29 +01:00
|
|
|
html! {
|
2025-10-29 23:29:27 +00:00
|
|
|
<button
|
|
|
|
|
onclick={on_click}
|
|
|
|
|
class={classes!(class, "add-role")}
|
|
|
|
|
>
|
|
|
|
|
<span>{name}</span>
|
|
|
|
|
</button>
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
2025-10-04 17:50:29 +01:00
|
|
|
})
|
|
|
|
|
.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>
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-04 17:50:29 +01:00
|
|
|
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(|| {
|
2025-10-07 01:47:59 +01:00
|
|
|
let assignments =
|
|
|
|
|
settings
|
|
|
|
|
.slots()
|
|
|
|
|
.iter()
|
|
|
|
|
.cloned()
|
|
|
|
|
.filter_map(|s| {
|
|
|
|
|
s.assign_to
|
2025-10-04 17:50:29 +01:00
|
|
|
.as_ref()
|
|
|
|
|
.map(|a| {
|
|
|
|
|
players
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|p| p.player_id == *a)
|
|
|
|
|
.map(|assign| {
|
2025-10-07 01:47:59 +01:00
|
|
|
let class = s.role.category().class();
|
|
|
|
|
(html! {
|
2025-10-04 17:50:29 +01:00
|
|
|
<Identity ident={assign.public.clone()}/>
|
2025-10-07 01:47:59 +01:00
|
|
|
}, Some(class))
|
2025-10-04 17:50:29 +01:00
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| {
|
2025-10-07 01:47:59 +01:00
|
|
|
(html! {
|
2025-10-04 17:50:29 +01:00
|
|
|
<span>{"[left the lobby]"}</span>
|
2025-10-07 01:47:59 +01:00
|
|
|
}, None)
|
2025-10-04 17:50:29 +01:00
|
|
|
})
|
|
|
|
|
})
|
2025-10-07 01:47:59 +01:00
|
|
|
.map(|(who, class)| {
|
2025-10-04 17:50:29 +01:00
|
|
|
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! {
|
2025-10-07 01:47:59 +01:00
|
|
|
<Button classes={classes!("assignment", class)} on_click={on_click}>
|
2025-10-04 17:50:29 +01:00
|
|
|
<label>{s.role.to_string().to_case(Case::Title)}</label>
|
|
|
|
|
{who}
|
|
|
|
|
</Button>
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-10-07 01:47:59 +01:00
|
|
|
})
|
|
|
|
|
.collect::<Html>();
|
2025-10-04 17:50:29 +01:00
|
|
|
|
|
|
|
|
html! {
|
|
|
|
|
<>
|
|
|
|
|
<label>{"assignments"}</label>
|
|
|
|
|
<div class="assignments">
|
|
|
|
|
{assignments}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-06-23 09:48:28 +01:00
|
|
|
|
2025-10-06 01:03:16 +01:00
|
|
|
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>
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-10 18:34:59 +01:00
|
|
|
let add_player_open = use_state(|| false);
|
|
|
|
|
let add_player_opts = html! {
|
2025-10-29 23:29:27 +00:00
|
|
|
<div class="add-player">
|
|
|
|
|
<Signin callback={on_add_player.clone()}/>
|
|
|
|
|
</div>
|
2025-10-10 18:34:59 +01:00
|
|
|
};
|
|
|
|
|
|
2025-06-23 09:48:28 +01:00
|
|
|
html! {
|
|
|
|
|
<div class="settings">
|
2025-10-04 17:50:29 +01:00
|
|
|
<div class="top-settings">
|
2025-10-07 01:47:59 +01:00
|
|
|
{qr_mode_button.clone()}
|
2025-10-06 01:03:16 +01:00
|
|
|
{fill_empty_with_villagers}
|
|
|
|
|
{clear_setup}
|
2025-10-04 17:50:29 +01:00
|
|
|
{clear_all_assignments}
|
|
|
|
|
{clear_bad_assigned}
|
|
|
|
|
</div>
|
2025-10-10 18:34:59 +01:00
|
|
|
<ClickableField
|
|
|
|
|
options={add_player_opts}
|
|
|
|
|
state={add_player_open}
|
|
|
|
|
>
|
|
|
|
|
{"add player"}
|
|
|
|
|
</ClickableField>
|
2025-10-04 17:50:29 +01:00
|
|
|
<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>
|
2025-10-07 01:47:59 +01:00
|
|
|
<div class="roles-in-setup">
|
|
|
|
|
<h3>{"roles in the game"}</h3>
|
|
|
|
|
<div class="role-list">
|
|
|
|
|
{roles}
|
|
|
|
|
</div>
|
2025-06-23 09:48:28 +01:00
|
|
|
</div>
|
2025-10-07 01:47:59 +01:00
|
|
|
{assignments}
|
2025-10-04 17:50:29 +01:00
|
|
|
<Button
|
|
|
|
|
disabled_reason={disabled_reason}
|
|
|
|
|
classes={classes!("start-game")}
|
|
|
|
|
on_click={on_start.clone()}
|
|
|
|
|
>
|
|
|
|
|
{"start game"}
|
|
|
|
|
</Button>
|
2025-06-23 09:48:28 +01:00
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 17:50:29 +01:00
|
|
|
pub enum SettingSlotAction {
|
|
|
|
|
Remove(SlotId),
|
|
|
|
|
Update(SetupSlot),
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq, Properties)]
|
2025-10-04 17:50:29 +01:00
|
|
|
pub struct SettingsSlotProps {
|
|
|
|
|
pub players_for_assign: Rc<[Identification]>,
|
|
|
|
|
pub roles_in_setup: Rc<[RoleTitle]>,
|
|
|
|
|
pub slot: SetupSlot,
|
|
|
|
|
pub update: Callback<SettingSlotAction>,
|
2025-10-10 18:34:59 +01:00
|
|
|
pub all_players: Rc<[Identification]>,
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[function_component]
|
2025-10-04 17:50:29 +01:00
|
|
|
pub fn SettingsSlot(
|
|
|
|
|
SettingsSlotProps {
|
|
|
|
|
players_for_assign,
|
|
|
|
|
roles_in_setup,
|
|
|
|
|
slot,
|
|
|
|
|
update,
|
2025-10-10 18:34:59 +01:00
|
|
|
all_players,
|
2025-10-04 17:50:29 +01:00
|
|
|
}: &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);
|
2025-06-23 09:48:28 +01:00
|
|
|
});
|
2025-10-04 17:50:29 +01:00
|
|
|
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());
|
2025-10-10 18:34:59 +01:00
|
|
|
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"}});
|
2025-10-04 17:50:29 +01:00
|
|
|
html! {
|
|
|
|
|
<>
|
|
|
|
|
<Button on_click={on_kick}>
|
|
|
|
|
{"remove"}
|
|
|
|
|
</Button>
|
|
|
|
|
<ClickableField
|
|
|
|
|
options={assign_to}
|
|
|
|
|
state={assign_open}
|
|
|
|
|
class={classes!("assign-list")}
|
|
|
|
|
>
|
2025-10-10 18:34:59 +01:00
|
|
|
{assign_text}
|
2025-10-04 17:50:29 +01:00
|
|
|
</ClickableField>
|
|
|
|
|
{options}
|
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let class = slot.role.category().class();
|
2025-06-23 09:48:28 +01:00
|
|
|
html! {
|
2025-10-04 17:50:29 +01:00
|
|
|
<ClickableField
|
|
|
|
|
class={classes!("setup-slot")}
|
|
|
|
|
button_class={classes!(class)}
|
|
|
|
|
options={submenu}
|
|
|
|
|
state={open}
|
|
|
|
|
with_backdrop_exit=true
|
|
|
|
|
>
|
|
|
|
|
<label>{role_name}</label>
|
|
|
|
|
</ClickableField>
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 17:50:29 +01:00
|
|
|
fn assign_to_submenu(
|
|
|
|
|
players: &[Identification],
|
|
|
|
|
slot: &SetupSlot,
|
|
|
|
|
update: &Callback<SettingSlotAction>,
|
|
|
|
|
assign_setter: &UseStateSetter<bool>,
|
|
|
|
|
) -> Html {
|
2025-10-10 18:34:59 +01:00
|
|
|
let buttons = players
|
2025-10-04 17:50:29 +01:00
|
|
|
.iter()
|
|
|
|
|
.map(|p| {
|
|
|
|
|
let slot = slot.clone();
|
|
|
|
|
let update = update.clone();
|
2025-10-05 10:54:47 +01:00
|
|
|
let pid = p.player_id;
|
2025-10-04 17:50:29 +01:00
|
|
|
let setter = assign_setter.clone();
|
|
|
|
|
let on_click = Callback::from(move |_| {
|
|
|
|
|
let mut slot = slot.clone();
|
2025-10-05 10:54:47 +01:00
|
|
|
slot.assign_to.replace(pid);
|
2025-10-04 17:50:29 +01:00
|
|
|
update.emit(SettingSlotAction::Update(slot));
|
|
|
|
|
setter.set(false);
|
|
|
|
|
});
|
|
|
|
|
html! {
|
|
|
|
|
<Button on_click={on_click}>
|
|
|
|
|
<Identity ident={p.public.clone()}/>
|
|
|
|
|
</Button>
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-10-10 18:34:59 +01:00
|
|
|
.collect::<Html>();
|
|
|
|
|
|
|
|
|
|
html! {
|
|
|
|
|
<div class="assignees">
|
|
|
|
|
{buttons}
|
|
|
|
|
</div>
|
|
|
|
|
}
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-04 17:50:29 +01:00
|
|
|
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 {
|
2025-10-06 20:45:15 +01:00
|
|
|
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>
|
|
|
|
|
</>
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-10-04 17:50:29 +01:00
|
|
|
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>
|
|
|
|
|
</>
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-10-06 01:03:16 +01:00
|
|
|
SetupRole::Apprentice { to: specifically } => {
|
2025-10-04 17:50:29 +01:00
|
|
|
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 {
|
2025-10-06 01:03:16 +01:00
|
|
|
SetupRole::Apprentice { to: specifically } => {
|
2025-10-04 17:50:29 +01:00
|
|
|
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 {
|
2025-10-06 01:03:16 +01:00
|
|
|
SetupRole::Apprentice { to: specifically } => {
|
2025-10-04 17:50:29 +01:00
|
|
|
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,
|
2025-06-23 09:48:28 +01:00
|
|
|
};
|
2025-10-04 17:50:29 +01:00
|
|
|
|
|
|
|
|
setup_options_for_role.unwrap_or_default()
|
2025-06-23 09:48:28 +01:00
|
|
|
}
|