246 lines
7.6 KiB
Rust
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>
|
|
}
|
|
}
|