diff --git a/werewolves-proto/src/aura.rs b/werewolves-proto/src/aura.rs index 6732a0b..1ca8789 100644 --- a/werewolves-proto/src/aura.rs +++ b/werewolves-proto/src/aura.rs @@ -53,8 +53,6 @@ pub enum Aura { /// getting a first result of wolf/killer/powerful if seer/adjudicator/power seer #[checks("assignable")] InevitableScapegoat, - #[checks("assignable")] - Notorious, } impl Display for Aura { @@ -69,7 +67,6 @@ impl Display for Aura { Aura::VindictiveScapegoat => "Vindictive Scapegoat", Aura::SpitefulScapegoat => "Spiteful Scapegoat", Aura::InevitableScapegoat => "Inevitable Scapegoat", - Aura::Notorious => "Notorious", }) } } @@ -82,7 +79,6 @@ impl Aura { | Aura::VindictiveScapegoat | Aura::SpitefulScapegoat | Aura::InevitableScapegoat - | Aura::Notorious | Aura::Traitor | Aura::Drunk(_) | Aura::Insane => false, @@ -171,8 +167,7 @@ impl Auras { for aura in self.0.iter() { match aura { Aura::Traitor => return Some(Alignment::Traitor), - Aura::Notorious - | Aura::RedeemableScapegoat + Aura::RedeemableScapegoat | Aura::VindictiveScapegoat | Aura::SpitefulScapegoat | Aura::Scapegoat @@ -212,7 +207,39 @@ impl AuraTitle { AuraTitle::VindictiveScapegoat => Aura::VindictiveScapegoat, AuraTitle::SpitefulScapegoat => Aura::SpitefulScapegoat, AuraTitle::InevitableScapegoat => Aura::InevitableScapegoat, - AuraTitle::Notorious => Aura::Notorious, + } + } + + /// returns a list of auras with the current aura added. + /// + /// **this will remove incompatible auras** + pub fn try_add_to_list(&self, list: &[Self]) -> Vec { + list.iter() + .filter(|o| *o != self && !self.incompatible_with(**o)) + .copied() + .chain(Some(*self)) + .collect() + } + + /// determines whether the `other` [AuraTitle] is incompatible with + /// this aura + pub fn incompatible_with(&self, other: Self) -> bool { + match self { + AuraTitle::Bloodlet | AuraTitle::Insane | AuraTitle::Drunk | AuraTitle::Traitor => { + false + } + AuraTitle::VindictiveScapegoat + | AuraTitle::RedeemableScapegoat + | AuraTitle::SpitefulScapegoat + | AuraTitle::InevitableScapegoat + | AuraTitle::Scapegoat => matches!( + other, + AuraTitle::Scapegoat + | AuraTitle::RedeemableScapegoat + | AuraTitle::SpitefulScapegoat + | AuraTitle::VindictiveScapegoat + | AuraTitle::InevitableScapegoat + ), } } } diff --git a/werewolves-proto/src/game/night/process.rs b/werewolves-proto/src/game/night/process.rs index 69fd795..45d60e1 100644 --- a/werewolves-proto/src/game/night/process.rs +++ b/werewolves-proto/src/game/night/process.rs @@ -643,7 +643,6 @@ impl Night { Aura::RedeemableScapegoat | Aura::VindictiveScapegoat | Aura::SpitefulScapegoat - | Aura::Notorious | Aura::Scapegoat | Aura::Traitor | Aura::Bloodlet { .. } => continue, diff --git a/werewolves-proto/src/game/settings/settings_role.rs b/werewolves-proto/src/game/settings/settings_role.rs index 59bdc1b..5b84b23 100644 --- a/werewolves-proto/src/game/settings/settings_role.rs +++ b/werewolves-proto/src/game/settings/settings_role.rs @@ -165,7 +165,6 @@ impl SetupRoleTitle { | AuraTitle::VindictiveScapegoat | AuraTitle::SpitefulScapegoat | AuraTitle::InevitableScapegoat - | AuraTitle::Notorious | AuraTitle::Traitor | AuraTitle::Bloodlet | AuraTitle::Insane => false, @@ -212,12 +211,10 @@ impl SetupRoleTitle { | SetupRoleTitle::Insomniac ) } - AuraTitle::Notorious => { - !matches!(self, SetupRoleTitle::Villager | SetupRoleTitle::Scapegoat) - } AuraTitle::RedeemableScapegoat => matches!(self, SetupRoleTitle::Villager), - AuraTitle::VindictiveScapegoat | AuraTitle::SpitefulScapegoat => true, - AuraTitle::Scapegoat => matches!(self, SetupRoleTitle::Villager), + AuraTitle::VindictiveScapegoat + | AuraTitle::SpitefulScapegoat + | AuraTitle::Scapegoat => !matches!(self, SetupRoleTitle::Scapegoat), } } pub fn into_role(self) -> Role { diff --git a/werewolves/index.scss b/werewolves/index.scss index 7d98efb..1b658df 100644 --- a/werewolves/index.scss +++ b/werewolves/index.scss @@ -264,7 +264,7 @@ nav.host-nav { width: 100%; } - &>label { + &>span { font-size: 1rem; margin-bottom: 0; } @@ -473,13 +473,17 @@ button.confirm { } .roles-in-setup { - border: 1px solid rgba(255, 255, 255, 0.6); - padding: 10px; + padding: 1rem; + border: 4px solid $village_color_faint; + background-color: color.change($village_color_faint, $alpha: 0.05); + + zoom: 120%; &>h3 { margin: 0; text-align: center; - color: rgba(255, 255, 255, 0.6); + color: white; + font-weight: normal; } } @@ -862,11 +866,9 @@ clients { } &.shown { - // visibility: visible; display: flex; flex-direction: row; align-items: baseline; - // position: absolute; } button { @@ -914,6 +916,31 @@ error { } } +.identity-span { + list-style: none; + + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 2px; + font-size: 1rem; + margin: 0px; + padding: 0px; + align-items: center; + + span { + margin: 0px; + padding: 0px; + text-wrap: nowrap; + overflow: hidden; + } + + .number { + padding-right: 5px; + font-size: 1.2em; + } +} + .binary { margin: 0; padding: 0; @@ -1133,6 +1160,52 @@ input { align-self: flex-end; } +.slot-container { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + gap: 3px; + font-size: 0.7em; + + .slot-options { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + gap: 3px; + align-items: center; + + opacity: 30%; + + &:hover { + opacity: 100%; + } + } +} + +.slot-auras { + padding-top: 5px; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-items: center; + gap: 2px; + + .slot-aura-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 3px; + + span::after { + content: ', '; + } + + span:last-child::after { + content: ''; + } + } +} + .increment-decrement { display: flex; flex-direction: row; @@ -1142,9 +1215,7 @@ input { align-items: center; align-content: center; - &>label { - // height: 100%; - // width: 100%; + &>span { flex-grow: 3; } @@ -1158,8 +1229,7 @@ input { .setup-slot { text-align: center; - & button label { - color: white; + & button span { cursor: pointer; } @@ -1167,6 +1237,7 @@ input { flex-direction: column; flex-wrap: nowrap; align-items: center; + gap: 3px; &>.submenu { width: 30vw; @@ -1199,6 +1270,19 @@ input { gap: 10px; font-size: 1.5rem; + .role-box { + width: 1.25em; + height: 1.25em; + display: flex; + align-items: center; + justify-content: center; + + &>* { + flex-shrink: 1; + width: 1em; + } + } + cursor: pointer; } @@ -1431,7 +1515,8 @@ input { display: flex; flex-direction: row; flex-wrap: wrap; - justify-content: space-around; + justify-content: flex-start; + gap: 5px; row-gap: 10px; font-size: 2em; @@ -1463,71 +1548,106 @@ input { border: 1px solid color.change($defensive_border, $lightness: 40%); } - .category { - margin-bottom: 30px; - width: 30%; - text-align: center; - display: flex; - flex-direction: column; - &.final { - margin-top: 1cm; - margin-bottom: 1cm; +} + + + +.category { + text-align: center; + display: flex; + flex-direction: column; + + &.add-list { + @media only screen and (max-width : 599px) { + width: 100%; } - & .title { - text-shadow: black 3px 2px; - margin-bottom: 10px; + @media only screen and (min-width : 600px) { + width: 160px; } - & .count { - text-align: right; - left: -40px; - position: relative; + .hidden { + display: none; + } - width: 0; - height: 0; + width: auto; + flex-wrap: wrap; + gap: 1px; - .scapegoats { - color: rgba(255, 0, 255, 0.7); - font-size: 2em; - position: absolute; - } + + &>.title { + font-size: 0.5em !important; + margin-bottom: 0px !important; + cursor: pointer; } .category-list { - text-align: left; - flex: 1, 1, 100%; - display: flex; - flex-wrap: nowrap; - flex-direction: column; - gap: 5px; + gap: 1px; + } + } - .slot { + &.big { + margin-bottom: 30px; + width: 30%; + } + + &.final { + margin-top: 1cm; + margin-bottom: 1cm; + } + + & .title { + text-shadow: black 3px 2px; + margin-bottom: 10px; + } + + & .count { + text-align: right; + left: -40px; + position: relative; + + width: 0; + height: 0; + + .scapegoats { + color: rgba(255, 0, 255, 0.7); + font-size: 2em; + position: absolute; + } + } + + .category-list { + text-align: left; + display: flex; + flex-wrap: nowrap; + flex-direction: column; + gap: 5px; + + .slot { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + + .attributes { + margin-left: 10px; + align-self: flex-end; display: flex; flex-direction: row; flex-wrap: nowrap; + gap: 10px; + } - .attributes { - margin-left: 10px; - align-self: flex-end; - display: flex; - flex-direction: row; - flex-wrap: nowrap; - gap: 10px; - } + .role { + text-shadow: black 3px 2px; - .role { - text-shadow: black 3px 2px; + width: 100%; + filter: saturate(40%); + padding-left: 10px; + padding-right: 10px; - width: 100%; - filter: saturate(40%); - padding-left: 10px; - padding-right: 10px; - - &.wakes { - border: 2px solid yellow; - } + &.wakes { + border: 2px solid yellow; } } } @@ -2521,3 +2641,10 @@ li.choice { } } } + +.option-menu { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + gap: 5px; +} diff --git a/werewolves/src/clients/host/host.rs b/werewolves/src/clients/host/host.rs index 9cc8ffb..09d64aa 100644 --- a/werewolves/src/clients/host/host.rs +++ b/werewolves/src/clients/host/host.rs @@ -42,9 +42,10 @@ use yew::{html::Scope, prelude::*}; use crate::{ callback, components::{ - Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Settings, Story, Victory, + Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Story, Victory, action::{ActionResultView, Prompt}, host::{CharacterStatesReadOnly, DaytimePlayerList, Setup}, + settings::Settings, }, pages::RolePage, storage::StorageKey, diff --git a/werewolves/src/components/aura.rs b/werewolves/src/components/aura.rs index 983c48f..563ff72 100644 --- a/werewolves/src/components/aura.rs +++ b/werewolves/src/components/aura.rs @@ -30,7 +30,6 @@ impl Class for AuraTitle { AuraTitle::RedeemableScapegoat | AuraTitle::SpitefulScapegoat | AuraTitle::VindictiveScapegoat - | AuraTitle::Notorious | AuraTitle::InevitableScapegoat | AuraTitle::Scapegoat => "scapegoat", }) diff --git a/werewolves/src/components/host/setup.rs b/werewolves/src/components/host/setup.rs index 55a44e4..b919a7a 100644 --- a/werewolves/src/components/host/setup.rs +++ b/werewolves/src/components/host/setup.rs @@ -100,7 +100,7 @@ pub fn Setup(SetupProps { settings }: &SetupProps) -> Html {
{categories}
-
+
{power_roles_count} {"Power roles from..."}
@@ -208,7 +208,7 @@ pub fn SetupCategory( }) .collect::(); html! { -
+
{roles_count}
{category.to_string().to_case(Case::Title)} diff --git a/werewolves/src/components/icon.rs b/werewolves/src/components/icon.rs index 5ace27f..30b33d1 100644 --- a/werewolves/src/components/icon.rs +++ b/werewolves/src/components/icon.rs @@ -241,8 +241,7 @@ impl PartialAssociatedIcon for AuraTitle { | AuraTitle::RedeemableScapegoat | AuraTitle::VindictiveScapegoat | AuraTitle::SpitefulScapegoat - | AuraTitle::InevitableScapegoat - | AuraTitle::Notorious => Some(IconSource::Scapegoat), + | AuraTitle::InevitableScapegoat => Some(IconSource::Scapegoat), } } } diff --git a/werewolves/src/components/identity.rs b/werewolves/src/components/identity.rs index 28bcd59..0a20f52 100644 --- a/werewolves/src/components/identity.rs +++ b/werewolves/src/components/identity.rs @@ -50,3 +50,33 @@ pub fn Identity(props: &IdentityProps) -> Html {
} } + +#[function_component] +pub fn IdentitySpan( + IdentityProps { + ident: + PublicIdentity { + name, + pronouns, + number, + }, + class, + }: &IdentityProps, +) -> Html { + let pronouns = pronouns.as_ref().map(|p| { + html! { + {"("}{p}{")"} + } + }); + let not_set = number.is_none().then_some("not-set"); + let number = number + .map(|n| n.to_string()) + .unwrap_or_else(|| String::from("???")); + html! { +
+ {number} + {name} + {pronouns} +
+ } +} diff --git a/werewolves/src/components/settings/category.rs b/werewolves/src/components/settings/category.rs new file mode 100644 index 0000000..df89955 --- /dev/null +++ b/werewolves/src/components/settings/category.rs @@ -0,0 +1,75 @@ +// Copyright (C) 2025 Emilis Bliūdžius +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +use convert_case::{Case, Casing}; +use werewolves_proto::role::RoleTitle; +use yew::prelude::*; + +use crate::components::Button; + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct AddRoleCategoryProps { + pub category: werewolves_proto::game::Category, + pub roles: Box<[RoleTitle]>, + pub add_role: Callback, +} + +#[function_component] +pub fn AddRoleCategory( + AddRoleCategoryProps { + category, + roles, + add_role, + }: &AddRoleCategoryProps, +) -> Html { + let class = category.class(); + let hidden = use_state(|| true); + + let roles = roles + .iter() + .copied() + .map(|title| { + let on_click = { + let add_role = add_role.clone(); + let role = title; + Callback::from(move |_| add_role.emit(role)) + }; + let name = title.to_string().to_case(Case::Title); + html! { +
+ +
+ } + }) + .collect::(); + + let on_toggle = { + let hidden = hidden.clone(); + Callback::from(move |_| hidden.set(!*hidden)) + }; + let hidden = (*hidden).then_some("hidden"); + + html! { +
+
+ {category.to_string().to_case(Case::Sentence)} +
+
+ {roles} +
+
+ } +} diff --git a/werewolves/src/components/settings/settings.rs b/werewolves/src/components/settings/settings.rs new file mode 100644 index 0000000..95529e8 --- /dev/null +++ b/werewolves/src/components/settings/settings.rs @@ -0,0 +1,350 @@ +// Copyright (C) 2025 Emilis Bliūdžius +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +use core::{num::NonZeroU8, ops::Not}; +use std::rc::Rc; + +use convert_case::{Case, Casing}; +use werewolves_proto::{ + aura::AuraTitle, + error::GameError, + game::{Category, GameSettings, OrRandom, SetupRole, SetupSlot, SlotId}, + message::{Identification, PlayerState, PublicIdentity}, + role::RoleTitle, +}; +use yew::prelude::*; + +use crate::{ + class::Class, + components::{ + Button, ClickableField, Icon, IconType, Identity, PartialAssociatedIcon, + client::Signin, + settings::{AddRoleCategory, SettingSlotAction, SettingsSlot}, + }, +}; + +#[derive(Debug, PartialEq, Properties)] +pub struct SettingsProps { + pub settings: GameSettings, + pub players_in_lobby: Rc<[PlayerState]>, + pub on_update: Callback, + pub on_start: Callback<()>, + pub on_add_player: Callback, + 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::>(); + 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::::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::>(); + let players_for_assign = players + .iter() + .filter(|p| !already_assigned_pids.contains(&p.player_id)) + .cloned() + .collect::>(); + 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! { + + } + }) + .collect::(); + + let add_roles_update = on_update.clone(); + + let add_role = { + let settings = settings.clone(); + let update = add_roles_update.clone(); + Callback::from(move |role: RoleTitle| { + let mut settings = (*settings).clone(); + settings.new_slot(role); + update.emit(settings); + }) + }; + + let add_roles_buttons = Category::ALL + .iter() + .copied() + .map(|cat| { + let roles = cat + .entire_category() + .into_iter() + .map(|c| c.into_role().title()) + .collect::>(); + html! { + + } + }) + .collect::(); + + 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! { + + } + }); + + 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::>() + .into_iter() + .for_each(|s| settings.update_slot(s)); + update.emit(settings); + }); + html! { + + } + }); + + 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! { + + }, Some(class)) + }) + .unwrap_or_else(|| { + (html! { + {"[left the lobby]"} + }, 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! { + + } + }) + }) + .collect::(); + + html! { + <> + +
+ {assignments} +
+ + } + }); + + 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! { + + } + }; + + 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! { + + } + }; + + let add_player_open = use_state(|| false); + let add_player_opts = html! { +
+ +
+ }; + let current_roles = settings.slots().len(); + let min_roles = settings.min_players_needed(); + let min_roles_class = (current_roles < min_roles).then_some("red"); + let player_count = players_in_lobby.len(); + let player_count_class = (player_count != current_roles).then_some("red"); + + html! { +
+
+ {qr_mode_button.clone()} + {fill_empty_with_villagers} + {clear_setup} + {clear_all_assignments} + {clear_bad_assigned} +
+ + {"add player"} + + +
+ {add_roles_buttons} +
+
+ + {"current/minimum roles: "} + + + {current_roles} + + {"/"} + + {min_roles} + + + + + {"players/roles: "} + + + {player_count} + + {"/"} + + {current_roles} + + + +
+
+

{"roles in the game"}

+
+ {roles} +
+
+ {assignments} + +
+ } +} diff --git a/werewolves/src/components/settings.rs b/werewolves/src/components/settings/slot.rs similarity index 57% rename from werewolves/src/components/settings.rs rename to werewolves/src/components/settings/slot.rs index 3f0a7b5..3799108 100644 --- a/werewolves/src/components/settings.rs +++ b/werewolves/src/components/settings/slot.rs @@ -12,348 +12,27 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . + use core::{num::NonZeroU8, ops::Not}; use std::rc::Rc; use convert_case::{Case, Casing}; use werewolves_proto::{ aura::AuraTitle, - error::GameError, - game::{GameSettings, OrRandom, SetupRole, SetupSlot, SlotId}, - message::{Identification, PlayerState, PublicIdentity}, + game::{OrRandom, SetupRole, SetupSlot, SlotId}, + message::Identification, role::RoleTitle, }; + use yew::prelude::*; use crate::{ class::Class, components::{ - Button, ClickableField, Icon, IconType, Identity, PartialAssociatedIcon, client::Signin, + Button, ClickableField, Icon, IconType, Identity, IdentitySpan, PartialAssociatedIcon, }, }; -#[derive(Debug, PartialEq, Properties)] -pub struct SettingsProps { - pub settings: GameSettings, - pub players_in_lobby: Rc<[PlayerState]>, - pub on_update: Callback, - pub on_start: Callback<()>, - pub on_add_player: Callback, - 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::>(); - 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::::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::>(); - let players_for_assign = players - .iter() - .filter(|p| !already_assigned_pids.contains(&p.player_id)) - .cloned() - .collect::>(); - 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! { - - } - }) - .collect::(); - - let add_roles_update = on_update.clone(); - let sorted_role_tiles = { - let mut v = RoleTitle::ALL.to_vec(); - v.sort_by_key(|v| Into::::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::::into(r).category().class(); - let name = r.to_string().to_case(Case::Title); - // let icon = r.icon().map(|icon| { - // html! { - // - // } - // }); - html! { - - } - }) - .collect::(); - - 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! { - - } - }); - - 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::>() - .into_iter() - .for_each(|s| settings.update_slot(s)); - update.emit(settings); - }); - html! { - - } - }); - - 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! { - - }, Some(class)) - }) - .unwrap_or_else(|| { - (html! { - {"[left the lobby]"} - }, 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! { - - } - }) - }) - .collect::(); - - html! { - <> - -
- {assignments} -
- - } - }); - - 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! { - - } - }; - - 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! { - - } - }; - - let add_player_open = use_state(|| false); - let add_player_opts = html! { -
- -
- }; - let current_roles = settings.slots().len(); - let min_roles = settings.min_players_needed(); - let min_roles_class = (current_roles < min_roles).then_some("red"); - let player_count = players_in_lobby.len(); - let player_count_class = (player_count != current_roles).then_some("red"); - - html! { -
-
- {qr_mode_button.clone()} - {fill_empty_with_villagers} - {clear_setup} - {clear_all_assignments} - {clear_bad_assigned} -
- - {"add player"} - - -
- {add_roles_buttons} -
-
- - {"current/minimum roles: "} - - - {current_roles} - - {"/"} - - {min_roles} - - - - - {"players/roles: "} - - - {player_count} - - {"/"} - - {current_roles} - - - -
-
-

{"roles in the game"}

-
- {roles} -
-
- {assignments} - -
- } -} - pub enum SettingSlotAction { Remove(SlotId), Update(SetupSlot), @@ -418,7 +97,7 @@ pub fn SettingsSlot( }) .unwrap_or_else(|| html! {{"assign"}}); html! { - <> +
@@ -430,20 +109,62 @@ pub fn SettingsSlot( {assign_text} {options} - +
} }; let class = slot.role.category().class(); + + let assigned_to = slot + .assign_to + .as_ref() + .copied() + .and_then(|assigned_to| all_players.iter().find(|p| p.player_id == assigned_to)) + .map(|ident| { + html! { + + } + }); + + let auras = slot.auras.is_empty().not().then_some({ + let list = slot + .auras + .iter() + .map(|aura| { + html! { + {aura.to_string()} + } + }) + .collect::(); + let title = (slot.auras.len() > 1).then_some(html! { + {"auras:"} + }); + html! { +
+ {title} +
+ {list} +
+
+ } + }); + let other_options = display_options_for_slot(slot); html! { - - - +
+ + {role_name} + +
+ {assigned_to} + {auras} + {other_options} +
+
} } @@ -481,6 +202,81 @@ fn assign_to_submenu( } } +fn display_options_for_slot(slot: &SetupSlot) -> Html { + let options = match &slot.role { + SetupRole::Seer + | SetupRole::Arcanist + | SetupRole::Gravedigger + | SetupRole::Hunter + | SetupRole::Militia + | SetupRole::MapleWolf + | SetupRole::Guardian + | SetupRole::Protector + | SetupRole::Werewolf + | SetupRole::AlphaWolf + | SetupRole::DireWolf + | SetupRole::Shapeshifter + | SetupRole::LoneWolf + | SetupRole::Bloodletter + | SetupRole::Adjudicator + | SetupRole::Insomniac + | SetupRole::PowerSeer + | SetupRole::Mortician + | SetupRole::Beholder + | SetupRole::Empath + | SetupRole::Vindicator + | SetupRole::Diseased + | SetupRole::BlackKnight + | SetupRole::Weightlifter + | SetupRole::PyreMaster + | SetupRole::Villager => return html! {}, + SetupRole::Elder { knows_on_night } => html! { + {"wakes night "}{knows_on_night.get()} + }, + SetupRole::MasonLeader { recruits_available } => html! { + {recruits_available.get()}{ + if recruits_available.get() == 1 { + " recruit" + } else { + " recruits" + } + } + }, + SetupRole::Apprentice { to: None } => html! { + {"random mentor"} + }, + SetupRole::Apprentice { to: Some(mentor) } => { + let class = Into::::into(*mentor).category().class(); + html! { + + {"apprentice to "} + {mentor.to_string().to_case(Case::Sentence)} + + } + } + SetupRole::Scapegoat { + redeemed: OrRandom::Random, + } => html! { + {"redemption random"} + }, + SetupRole::Scapegoat { + redeemed: OrRandom::Determined(false), + } => html! { + {"not redeemed"} + }, + SetupRole::Scapegoat { + redeemed: OrRandom::Determined(true), + } => html! { + {"redeemed"} + }, + }; + html! { +
+ {options} +
+ } +} + fn setup_options_for_slot( slot: &SetupSlot, update: &Callback, @@ -506,7 +302,7 @@ fn setup_options_for_slot( if aura_active { slot.auras.retain(|a| *a != aura); } else { - slot.auras.push(aura); + slot.auras = aura.try_add_to_list(&slot.auras); } update.emit(SettingSlotAction::Update(slot)) }) @@ -547,7 +343,7 @@ fn setup_options_for_slot( state={open_aura_assign} options={options} > - {"auras"} + {slot.auras.len()}{" auras"} } }) @@ -597,10 +393,10 @@ fn setup_options_for_slot( let decrement_disabled_reason = prev.is_none().then_some("at minimum"); Some(html! { <> - + {"recruits"}
- + {recruits_available.get().to_string()}
@@ -630,9 +426,9 @@ fn setup_options_for_slot( }; Some(html! { <> - + {"redeemed?"} }) @@ -751,10 +547,10 @@ fn setup_options_for_slot( }); Some(html! { <> - + {"knows on night"}
- + {knows_on_night.to_string()}
diff --git a/werewolves/src/main.rs b/werewolves/src/main.rs index 15b91a8..5bf9aeb 100644 --- a/werewolves/src/main.rs +++ b/werewolves/src/main.rs @@ -31,6 +31,9 @@ mod components { pub mod action { werewolves_macros::include_path!("werewolves/src/components/action"); } + pub mod settings { + werewolves_macros::include_path!("werewolves/src/components/settings"); + } } mod pages { werewolves_macros::include_path!("werewolves/src/pages");