From e168bbd9b206ce84ea197dc8221502aa310f1fa5 Mon Sep 17 00:00:00 2001 From: emilis Date: Tue, 3 Feb 2026 01:36:05 +0000 Subject: [PATCH] feat: basic vote counter --- werewolves/index.scss | 75 ++++++++++++ werewolves/src/clients/host/host.rs | 42 ++++++- werewolves/src/components/host/vote.rs | 157 +++++++++++++++++++++++++ 3 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 werewolves/src/components/host/vote.rs diff --git a/werewolves/index.scss b/werewolves/index.scss index 071df58..bb42dbe 100644 --- a/werewolves/index.scss +++ b/werewolves/index.scss @@ -158,6 +158,16 @@ nav.host-nav { background-color: white; color: color.invert(#cccccc); } + + &.mode-set { + background-color: white; + color: black; + + &:hover { + background-color: rgba(255, 255, 255, 0.3); + color: #cccccc, + } + } } .player-list, @@ -3027,3 +3037,68 @@ dialog { flex-grow: 1; } } + +.vote-summary { + list-style: none; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + font-size: 2em; + + li { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: baseline; + } +} + +.voting-mode { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + gap: 1ch; + font-size: 2em; + align-items: center; + + .mode-buttons { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + flex-grow: 1; + width: 100%; + + &>button { + flex-grow: 1; + } + } +} + +.vote-char { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: baseline; +} + +.voter-id { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 1ch; + + .number { + font-weight: bold; + } + + .name { + font-style: italic; + } +} + +.vote-char { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-items: center; +} diff --git a/werewolves/src/clients/host/host.rs b/werewolves/src/clients/host/host.rs index 2535279..154cb2e 100644 --- a/werewolves/src/clients/host/host.rs +++ b/werewolves/src/clients/host/host.rs @@ -44,7 +44,7 @@ use crate::{ components::{ Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Victory, action::{ActionResultView, Prompt}, - host::{CharacterStatesReadOnly, DaytimePlayerList, Setup}, + host::{CharacterStatesReadOnly, DaytimePlayerList, Setup, VotingMode}, settings::Settings, story::Story, }, @@ -230,6 +230,10 @@ pub enum HostState { ScreenOverrides { return_to: Box, }, + VotingMode { + return_to: Box, + characters: Box<[CharacterState]>, + }, Story { story: GameStory, #[allow(unused)] @@ -324,6 +328,11 @@ impl Component for Host { fn view(&self, _ctx: &Context) -> Html { let content = match self.state.clone() { + HostState::VotingMode { characters, .. } => { + html! { + + } + } HostState::ScreenOverrides { .. } => { let send = { let send = self.send.clone(); @@ -499,6 +508,35 @@ impl Component for Host { } } }; + let voting_mode_btn = { + match &self.state { + HostState::Day { characters, .. } => { + let on_vote_mode = { + let scope = _ctx.link().clone(); + let state = self.state.clone(); + let characters = characters.clone(); + move |_| { + scope.send_message(HostEvent::SetState(HostState::VotingMode { + return_to: Box::new(state.clone()), + characters: characters.clone(), + })) + } + }; + + Some(html! { + + }) + } + HostState::VotingMode { .. } => { + let back = + crate::callback::send_message(HostMessage::GetState, self.send.clone()); + Some(html! { + + }) + } + _ => None, + } + }; let view_roles_btn = match &self.state { HostState::Prompt(_, _) | HostState::Result(_, _) => { let on_view_click = crate::callback::send_message( @@ -585,6 +623,7 @@ impl Component for Host { } _ => None, }; + log::info!("voting mode button: {}", voting_mode_btn.is_some()); let nav = self.big_screen.not().then(|| { html! { } }); diff --git a/werewolves/src/components/host/vote.rs b/werewolves/src/components/host/vote.rs new file mode 100644 index 0000000..a9ffea6 --- /dev/null +++ b/werewolves/src/components/host/vote.rs @@ -0,0 +1,157 @@ +// Copyright (C) 2026 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 werewolves_proto::message::CharacterState; +use yew::prelude::*; + +use crate::components::{Button, IdentitySpan}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum VotingActionMode { + Add, + Sub, +} + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct VotingModeProps { + pub characters: Box<[CharacterState]>, +} + +#[function_component] +pub fn VotingMode(VotingModeProps { characters }: &VotingModeProps) -> Html { + let votes = use_state(|| { + characters + .iter() + .filter(|c| c.died_to.is_none()) + .map(|c| (c.clone(), 0u32)) + .collect::>() + }); + let voting_mode = use_state(|| VotingActionMode::Add); + let buttons = votes + .iter() + .filter_map(|(c, v)| { + c.died_to.is_none().then_some({ + let on_select = { + let cid = c.identity.character_id; + let votes = votes.clone(); + let voting_mode = voting_mode.clone(); + move |_| { + let mut v = (*votes).clone(); + if let Some((_, votes)) = + v.iter_mut().find(|(c, _)| c.identity.character_id == cid) + { + match *voting_mode { + VotingActionMode::Add => *votes = votes.saturating_add(1), + VotingActionMode::Sub => *votes = votes.saturating_sub(1), + } + } + votes.set(v); + } + }; + html! { + + } + }) + }) + .collect::(); + let set_add = { + let mode = voting_mode.clone(); + move |_| mode.set(VotingActionMode::Add) + }; + let set_sub = { + let mode = voting_mode.clone(); + move |_| mode.set(VotingActionMode::Sub) + }; + const MODE_CLASS: &str = "mode-set"; + let (add, sub) = match *voting_mode { + VotingActionMode::Add => (Some(MODE_CLASS), None), + VotingActionMode::Sub => (None, Some(MODE_CLASS)), + }; + let mut sorted = votes.iter().filter(|(_, v)| *v > 0).collect::>(); + sorted.sort_by_key(|(_, v)| *v); + sorted.reverse(); + let summary = sorted + .into_iter() + .map(|(c, v)| { + html! { +
  • + + {": "} + {*v} + {" votes"} +
  • + } + }) + .collect::(); + html! { +
    +
    +
    + {"voting mode"} +
    + + +
    +
    +
    +
    + {buttons} +
    +
      + {summary} +
    +
    + } +} + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct VotingPlayerProps { + pub character: CharacterState, + #[prop_or_default] + pub votes: u32, + pub on_select: Callback<()>, +} + +#[function_component] +pub fn VotingPlayer( + VotingPlayerProps { + on_select, + votes, + character: CharacterState { identity, .. }, + }: &VotingPlayerProps, +) -> Html { + let on_click = { + let on_select = on_select.clone(); + move |_| on_select.emit(()) + }; + let identity = identity.clone().into_public(); + let number = identity.number.as_ref().map(|n| { + html! { + {n.get()} + } + }); + html! { + + } +}