feat: basic vote counter
This commit is contained in:
parent
241420757e
commit
e168bbd9b2
|
|
@ -158,6 +158,16 @@ nav.host-nav {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: color.invert(#cccccc);
|
color: color.invert(#cccccc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mode-set {
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
|
color: #cccccc,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-list,
|
.player-list,
|
||||||
|
|
@ -3027,3 +3037,68 @@ dialog {
|
||||||
flex-grow: 1;
|
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::{
|
components::{
|
||||||
Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Victory,
|
Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Victory,
|
||||||
action::{ActionResultView, Prompt},
|
action::{ActionResultView, Prompt},
|
||||||
host::{CharacterStatesReadOnly, DaytimePlayerList, Setup},
|
host::{CharacterStatesReadOnly, DaytimePlayerList, Setup, VotingMode},
|
||||||
settings::Settings,
|
settings::Settings,
|
||||||
story::Story,
|
story::Story,
|
||||||
},
|
},
|
||||||
|
|
@ -230,6 +230,10 @@ pub enum HostState {
|
||||||
ScreenOverrides {
|
ScreenOverrides {
|
||||||
return_to: Box<HostState>,
|
return_to: Box<HostState>,
|
||||||
},
|
},
|
||||||
|
VotingMode {
|
||||||
|
return_to: Box<HostState>,
|
||||||
|
characters: Box<[CharacterState]>,
|
||||||
|
},
|
||||||
Story {
|
Story {
|
||||||
story: GameStory,
|
story: GameStory,
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
|
@ -324,6 +328,11 @@ impl Component for Host {
|
||||||
|
|
||||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||||
let content = match self.state.clone() {
|
let content = match self.state.clone() {
|
||||||
|
HostState::VotingMode { characters, .. } => {
|
||||||
|
html! {
|
||||||
|
<VotingMode characters={characters.clone()}/>
|
||||||
|
}
|
||||||
|
}
|
||||||
HostState::ScreenOverrides { .. } => {
|
HostState::ScreenOverrides { .. } => {
|
||||||
let send = {
|
let send = {
|
||||||
let send = self.send.clone();
|
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 {
|
let view_roles_btn = match &self.state {
|
||||||
HostState::Prompt(_, _) | HostState::Result(_, _) => {
|
HostState::Prompt(_, _) | HostState::Result(_, _) => {
|
||||||
let on_view_click = crate::callback::send_message(
|
let on_view_click = crate::callback::send_message(
|
||||||
|
|
@ -585,6 +623,7 @@ impl Component for Host {
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
log::info!("voting mode button: {}", voting_mode_btn.is_some());
|
||||||
let nav = self.big_screen.not().then(|| {
|
let nav = self.big_screen.not().then(|| {
|
||||||
html! {
|
html! {
|
||||||
<nav class="host-nav" style="z-index: 3;">
|
<nav class="host-nav" style="z-index: 3;">
|
||||||
|
|
@ -592,6 +631,7 @@ impl Component for Host {
|
||||||
{view_roles_btn}
|
{view_roles_btn}
|
||||||
{override_screens_btn}
|
{override_screens_btn}
|
||||||
{skip_btn}
|
{skip_btn}
|
||||||
|
{voting_mode_btn}
|
||||||
</nav>
|
</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