diff --git a/style/icon.scss b/style/icon.scss index 9c3cee4..10272b5 100644 --- a/style/icon.scss +++ b/style/icon.scss @@ -1,5 +1,5 @@ .icon-fit { - // height: 1em; + height: 1em; flex-grow: 1; flex-shrink: 1; diff --git a/style/main.scss b/style/main.scss index a2841b2..de63852 100644 --- a/style/main.scss +++ b/style/main.scss @@ -935,3 +935,96 @@ form { gap: 1ch; font-size: 1.25em; } + + +.top-of-day-info { + width: max-content; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: flex-start; + padding: 10px; + + gap: 10vw; + + .info-tidbit { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-items: center; + gap: 5px; + + label { + font-size: 1.5em; + opacity: 70%; + } + + .parity { + font-size: 2em; + } + + .parity-pct { + font-size: 1.25em; + } + + .last-nights-kills { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 2cm; + + .identity { + .number { + color: red; + font-size: 1.5em; + } + + text-align: center; + font-size: 1.25em; + } + } + + .current-day { + color: red; + font-size: 3em; + flex-grow: 1; + } + } +} + +.player-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 0.5ch; + padding-bottom: 1ch; + + .character { + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1ch; + padding: 1ch; + font-size: 1.25em; + + .headline { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + gap: 0.5ch; + + align-items: center; + justify-content: center; + } + + background-color: $village_color_faint; + border: 1px solid $village_border_faint; + + &.marked { + background-color: color.change($red2, $alpha: 0.3); + border: 1px solid color.change($red2, $alpha: 0.8); + } + } +} diff --git a/style/night.scss b/style/night.scss index d0f5d94..026a882 100644 --- a/style/night.scss +++ b/style/night.scss @@ -1,4 +1,5 @@ .cover-of-darkness { + background-color: black; font-size: 3em; position: fixed; top: 0; diff --git a/werewolves/src/app/pages/game/big.rs b/werewolves/src/app/pages/game/big.rs index a19e91a..c923abf 100644 --- a/werewolves/src/app/pages/game/big.rs +++ b/werewolves/src/app/pages/game/big.rs @@ -164,12 +164,10 @@ pub fn BigScreen() -> impl IntoView { } } ServerToHostMessage::Disconnect => disconnect.set(true), - ServerToHostMessage::Daytime { - characters, - marked, - day, - settings, - } => todo!(), + ServerToHostMessage::Daytime { settings: s, .. } => { + settings.set(s); + page.set(BigScreenPage::Setup); + } ServerToHostMessage::PlayerStates(_) => {} ServerToHostMessage::ActionPrompt(act, ppage) => { page.set(BigScreenPage::ActionPrompt { @@ -229,10 +227,15 @@ pub fn BigScreen() -> impl IntoView { BigScreenPage::ActionPrompt { prompt, page } => { view! { }.into_any() } - BigScreenPage::ActionResult { character, result } => view! { } - .into_any(), + BigScreenPage::ActionResult { character, result } => { + view! { }.into_any() + } }; - view! {
{content}
}.into_any() + view! { +
{content}
+ }.into_any() } } diff --git a/werewolves/src/app/pages/game/host.rs b/werewolves/src/app/pages/game/host.rs index c6bc66f..7946e03 100644 --- a/werewolves/src/app/pages/game/host.rs +++ b/werewolves/src/app/pages/game/host.rs @@ -1,16 +1,15 @@ werewolves_macros::include_path!("werewolves/src/app/pages/game/host"); +use core::num::NonZeroU8; use std::collections::HashMap; use leptos::prelude::*; use werewolves_proto::{ + character::CharacterId, game::{Category, GameSettings}, message::{ - CharacterIdentity, PlayerState, - host::{ - HostGameMessage, HostLobbyMessage, HostMessage, HostNightMessage, - ServerToHostMessage as Srv2Host, - }, + CharacterIdentity, CharacterState, PlayerState, + host::{HostGameMessage, HostLobbyMessage, HostMessage, ServerToHostMessage as Srv2Host}, night::{ActionPrompt, ActionResult}, }, }; @@ -36,6 +35,12 @@ enum HostPage { character: Option, result: ActionResult, }, + Daytime { + day: NonZeroU8, + characters: Box<[CharacterState]>, + marked: Box<[CharacterId]>, + reply: WriteSignal>, + }, } #[component] @@ -118,6 +123,19 @@ pub fn HostGamePage( Srv2Host::ActionResult(character, result) => { page.set(HostPage::ActionResult { character, result }) } + Srv2Host::Daytime { + characters, + marked, + day, + .. + } => { + page.set(HostPage::Daytime { + day, + characters, + marked, + reply, + }); + } _ => log::error!("{message:#?}"), } } @@ -165,6 +183,15 @@ pub fn HostGamePage( view! { } .into_any() } + HostPage::Daytime { + day, + characters, + marked, + reply, + } => view! { + + } + .into_any(), }; view! { {cancel} diff --git a/werewolves/src/app/pages/game/host/daytime.rs b/werewolves/src/app/pages/game/host/daytime.rs new file mode 100644 index 0000000..4fd08c3 --- /dev/null +++ b/werewolves/src/app/pages/game/host/daytime.rs @@ -0,0 +1,187 @@ +// Copyright (C) 2025-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 . +use core::{num::NonZeroU8, ops::Not}; + +use convert_case::{Case, Casing}; +use leptos::prelude::*; +use leptos::{IntoView, prelude::WriteSignal}; +use werewolves_proto::message::host::{HostDayMessage, HostGameMessage}; +use werewolves_proto::{ + character::CharacterId, + game::GameTime, + message::{CharacterState, host::HostMessage}, +}; + +use crate::app::components::{ + AssociatedIcon, DialogModal, Icon, IconType, IdentityInline, PartialAssociatedIcon, +}; + +#[component] +pub fn DaytimePlayerList( + #[prop(optional)] day: Option, + characters: Box<[CharacterState]>, + marked: Box<[CharacterId]>, + reply: WriteSignal>, +) -> impl IntoView { + 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| { + view! { + + + + } + }) + .collect::>(); + kills.is_empty().not().then_some(view! { +
+ +
{kills.into_iter().collect_view()}
+
+ }) + }; + characters.sort_by(|l, r| l.identity.number.cmp(&r.identity.number)); + let chars = characters + .iter() + .map(|c| { + view! { + + } + }) + .collect_view(); + let (button_text, confirmation_text) = if marked.is_empty() { + ( + "end day".to_string(), + "really end the day with no executions?".to_string(), + ) + } else if marked.len() == 1 { + ( + "execute 1 player".to_string(), + characters + .iter() + .find(|c| c.identity.character_id == marked[0]) + .map(|c| c.identity.clone().into_public()) + .map(|id| format!("really execute {id}?")) + .unwrap_or("really execute 1 player?".to_string()), + ) + } else { + ( + format!("execute {} players", marked.len()), + format!("really execute {} players?", marked.len()), + ) + }; + 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(); + view! { +
+ + + {wolves} + {"/"} + {total} + + {"("} {pct_parity} {"%)"} +
+ } + }; + let button = view! { + +

{confirmation_text.clone()}

+ +
+ }; + let day = day.as_ref().map(|day| { + view! { +
+ + {day.get()} +
+ } + }); + view! { +
+
{day} {parity} {last_nights_kills}
+
{chars}
+ {button} +
+ } +} + +#[component] +fn DaytimePlayer( + character: CharacterState, + reply: WriteSignal>, + marked: bool, +) -> impl IntoView { + let CharacterState { + identity, + role, + died_to, + .. + } = character; + let character_id = identity.character_id; + let select = move |_| { + reply.set(Some(HostMessage::InGame(HostGameMessage::Day( + HostDayMessage::MarkForExecution(character_id), + )))) + }; + + 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"); + view! { + + } +} diff --git a/werewolves/src/app/pages/night_actions/result.rs b/werewolves/src/app/pages/night_actions/result.rs index 2b6df82..0130688 100644 --- a/werewolves/src/app/pages/night_actions/result.rs +++ b/werewolves/src/app/pages/night_actions/result.rs @@ -23,21 +23,39 @@ pub fn RoleResult( #[prop(optional)] reply: Option>>, ) -> impl IntoView { let body = match result { - ActionResult::RoleBlocked => view! { }.into_any(), - ActionResult::Drunk => view! { }.into_any(), - ActionResult::Seer(target, alignment) => view! { }.into_any(), - ActionResult::PowerSeer { target, powerful } => view! { }.into_any(), - ActionResult::Adjudicator { target, killer } => view! { }.into_any(), + ActionResult::RoleBlocked => view!{ + + }.into_any(), + ActionResult::Drunk => view! { + + }.into_any(), + ActionResult::Seer(target, alignment) => view!{ + + }.into_any(), + ActionResult::PowerSeer { target, powerful } => view!{ + + }.into_any(), + ActionResult::Adjudicator { target, killer } => view!{ + + }.into_any(), ActionResult::Arcanist((target1, target2), alignment_eq) => view! { }.into_any(), - ActionResult::GraveDigger(target, role) => view! { }.into_any(), - ActionResult::Mortician(target, died_to) => view! { }.into_any(), - ActionResult::Insomniac(visits) => view! { }.into_any(), - ActionResult::Empath { target, scapegoat } => view! { }.into_any(), + ActionResult::GraveDigger(target, role) => view! { + + }.into_any(), + ActionResult::Mortician(target, died_to) => view!{ + + }.into_any(), + ActionResult::Insomniac(visits) => view!{ + + }.into_any(), + ActionResult::Empath { target, scapegoat } => view! { + + }.into_any(), ActionResult::BeholderSawNothing => todo!(), ActionResult::BeholderSawEverything => todo!(), ActionResult::GoBackToSleep => return match reply { @@ -51,9 +69,7 @@ pub fn RoleResult( let Some(reply) = reply else { return; }; - reply.set(Some(HostNightMessage::ActionResponse( - ActionResponse::ContinueToResult, - ))); + reply.set(Some(HostNightMessage::Next)); }); ().into_any() } diff --git a/werewolves/src/main.rs b/werewolves/src/main.rs index 0884635..f896b0e 100644 --- a/werewolves/src/main.rs +++ b/werewolves/src/main.rs @@ -1,5 +1,5 @@ #![allow(clippy::expect_fun_call)] - +#![allow(clippy::boxed_local)] #[cfg(feature = "ssr")] mod ssr { pub const DEFAULT_MAX_PG_CONNECTIONS: u32 = 30;