werewolves/werewolves/src/components/host/daytime.rs

246 lines
7.6 KiB
Rust

// 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 <https://www.gnu.org/licenses/>.
use core::{num::NonZeroU8, ops::Not};
use convert_case::{Case, Casing};
use werewolves_proto::{
character::CharacterId,
game::GameTime,
message::{CharacterState, PublicIdentity},
};
use yew::prelude::*;
use crate::components::{AssociatedIcon, Button, Icon, IconType, Identity, PartialAssociatedIcon};
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct DaytimePlayerListProps {
#[prop_or_default]
pub day: Option<NonZeroU8>,
pub characters: Box<[CharacterState]>,
pub marked: Box<[CharacterId]>,
#[prop_or_default]
pub on_execute: Option<Callback<()>>,
pub on_mark: Callback<CharacterId>,
pub big_screen: bool,
}
#[function_component]
pub fn DaytimePlayerList(
DaytimePlayerListProps {
day,
characters,
on_execute,
on_mark,
marked,
big_screen,
}: &DaytimePlayerListProps,
) -> Html {
let on_select = big_screen.not().then(|| on_mark.clone());
let mut characters = characters.clone();
let last_nights_kills = {
let kills = characters
.iter()
.filter_map(|c| {
c.died_to
.as_ref()
.and_then(|died_to| match died_to.date_time() {
GameTime::Day { .. } => None,
GameTime::Night { number } => {
if let Some(day) = day.as_ref()
&& number == day.get() - 1
{
Some(c)
} else {
None
}
}
})
})
.map(|killed| {
let ident = killed.identity.clone().into_public();
html! {
<span>
<Identity ident={ident} />
</span>
}
})
.collect::<Box<[_]>>();
kills.is_empty().not().then_some(html! {
<div class="info-tidbit">
<label>{"died last night"}</label>
<div class="last-nights-kills">
{kills.into_iter().collect::<Html>()}
</div>
</div>
})
};
characters.sort_by(|l, r| l.identity.number.cmp(&r.identity.number));
let chars = characters
.iter()
.map(|c| {
let mark_state = match c.died_to.as_ref() {
None => marked
.contains(&c.identity.character_id)
.then_some(MarkState::Marked),
Some(died_to) => match died_to.date_time() {
GameTime::Day { .. } => Some(MarkState::Dead),
GameTime::Night { number } => {
if let Some(day) = day.as_ref()
&& number == day.get() - 1
{
Some(MarkState::DiedLastNight)
} else {
Some(MarkState::Dead)
}
}
},
};
html! {
<DaytimePlayer
character={c.clone()}
mark_state={mark_state}
on_select={on_select.clone()}
/>
}
})
.collect::<Html>();
let button_text = if marked.is_empty() {
"end day"
} else {
"execute"
};
let parity = {
let wolves = characters
.iter()
.filter(|c| c.died_to.is_none() && c.role.wolf())
.count();
let total = characters.iter().filter(|c| c.died_to.is_none()).count();
let pct_parity = (((wolves as f64) * 100.0) / (total as f64)).round();
html! {
<div class="info-tidbit">
<label>{"parity"}</label>
<span class="parity">
<span class="red">{wolves}</span>
{"/"}
<span class="total">{total}</span>
</span>
<span class="parity-pct">
{"("}
{pct_parity}
{"%)"}
</span>
</div>
}
};
let button = big_screen
.not()
.then_some(())
.and_then(|_| on_execute.clone())
.map(|on_execute| {
html! {
<Button on_click={on_execute}>
{button_text}
</Button>
}
});
let day = day.as_ref().map(|day| {
html! {
<div class="info-tidbit">
<label>{"day"}</label>
<span class="current-day">{day.get()}</span>
</div>
}
});
html! {
<div class="character-picker">
<div class="top-of-day-info">
{day}
{parity}
{last_nights_kills}
</div>
<div class="player-list">
{chars}
</div>
{button}
</div>
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum MarkState {
Marked,
DiedLastNight,
Dead,
}
impl MarkState {
pub const fn class(&self) -> &'static str {
match self {
MarkState::Marked => "marked",
MarkState::DiedLastNight => "recent-death",
MarkState::Dead => "dead",
}
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct DaytimePlayerProps {
pub character: CharacterState,
#[prop_or_default]
pub mark_state: Option<MarkState>,
pub on_select: Option<Callback<CharacterId>>,
}
#[function_component]
pub fn DaytimePlayer(
DaytimePlayerProps {
on_select,
mark_state,
character:
CharacterState {
player_id: _,
role,
died_to,
identity,
},
}: &DaytimePlayerProps,
) -> Html {
let class = mark_state.as_ref().map(|s| s.class());
let character_id = identity.character_id;
let on_click: Callback<_> = died_to
.is_none()
.then_some(())
.and(
on_select
.clone()
.map(|on_select| Callback::from(move |_| on_select.emit(character_id))),
)
.unwrap_or_default();
let identity: PublicIdentity = identity.into();
let icon = role.icon().unwrap_or_else(|| role.alignment().icon());
let text = role.to_string().to_case(Case::Title);
let align_class = role.wolf().then_some("red");
html! {
<Button on_click={on_click} classes={classes!(class, "character")}>
<div class="day-char">
<span class={classes!("headline")}>
<Icon source={icon} icon_type={IconType::Small}/>
<span class={classes!(align_class)}>{text}</span>
</span>
<Identity ident={identity}/>
</div>
</Button>
}
}