feat: basic vote counter
This commit is contained in:
parent
241420757e
commit
e168bbd9b2
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue