feat: basic vote counter

This commit is contained in:
emilis 2026-02-03 01:36:05 +00:00
parent 241420757e
commit e168bbd9b2
No known key found for this signature in database
3 changed files with 273 additions and 1 deletions

View File

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

View File

@ -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<HostState>,
},
VotingMode {
return_to: Box<HostState>,
characters: Box<[CharacterState]>,
},
Story {
story: GameStory,
#[allow(unused)]
@ -324,6 +328,11 @@ impl Component for Host {
fn view(&self, _ctx: &Context<Self>) -> Html {
let content = match self.state.clone() {
HostState::VotingMode { characters, .. } => {
html! {
<VotingMode characters={characters.clone()}/>
}
}
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! {
<Button on_click={on_vote_mode}>{"voting mode"}</Button>
})
}
HostState::VotingMode { .. } => {
let back =
crate::callback::send_message(HostMessage::GetState, self.send.clone());
Some(html! {
<Button on_click={back}>{"back"}</Button>
})
}
_ => 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! {
<nav class="host-nav" style="z-index: 3;">
@ -592,6 +631,7 @@ impl Component for Host {
{view_roles_btn}
{override_screens_btn}
{skip_btn}
{voting_mode_btn}
</nav>
}
});

View File

@ -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 <https://www.gnu.org/licenses/>.
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::<Box<[_]>>()
});
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! {
<VotingPlayer character={c.clone()} votes={*v} on_select={on_select}/>
}
})
})
.collect::<Html>();
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::<Box<_>>();
sorted.sort_by_key(|(_, v)| *v);
sorted.reverse();
let summary = sorted
.into_iter()
.map(|(c, v)| {
html! {
<li>
<IdentitySpan ident={c.identity.clone().into_public()}/>
{": "}
{*v}
{" votes"}
</li>
}
})
.collect::<Html>();
html! {
<div class="character-picker">
<div class="top-of-day-info">
<div class="voting-mode">
<span class="red">{"voting mode"}</span>
<div class="mode-buttons">
<Button classes={classes!(add)} on_click={set_add}>{"add"}</Button>
<Button classes={classes!(sub)} on_click={set_sub}>{"sub"}</Button>
</div>
</div>
</div>
<div class="player-list">
{buttons}
</div>
<ol class="vote-summary">
{summary}
</ol>
</div>
}
}
#[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! {
<span class="number">{n.get()}</span>
}
});
html! {
<Button on_click={on_click} classes={classes!("vote-char")}>
<span class="voter-id">
{number}
<span class="name">{identity.name.clone()}</span>
</span>
<span class="votes">
<span class="red">{*votes}</span>
{" votes"}
</span>
</Button>
}
}