From 80859f58d0fc0373eb670ad021aaf25294a45bb6 Mon Sep 17 00:00:00 2001 From: emilis Date: Fri, 20 Feb 2026 22:37:31 +0000 Subject: [PATCH] night prompts/actions --- style/big-screen.scss | 12 +- style/icon.scss | 6 +- style/main.scss | 17 + style/night.scss | 195 +++++++++++ style/setup.scss | 1 - werewolves-proto/src/message/night.rs | 42 ++- werewolves/src/app.rs | 8 + werewolves/src/app/components/cover.rs | 30 ++ werewolves/src/app/components/identity.rs | 25 +- werewolves/src/app/components/modal.rs | 2 +- werewolves/src/app/components/role.rs | 50 +++ werewolves/src/app/components/sample.rs | 2 +- werewolves/src/app/error.rs | 2 + werewolves/src/app/pages/game/big.rs | 43 ++- .../src/app/pages/game/big/role_reveal.rs | 3 +- werewolves/src/app/pages/game/big/setup.rs | 4 +- werewolves/src/app/pages/game/host.rs | 53 ++- .../app/pages/game/host/role_reveal_acks.rs | 3 +- .../src/app/pages/game/host/settings.rs | 18 +- .../src/app/pages/night_actions/damned.rs | 29 ++ .../src/app/pages/night_actions/drunk_page.rs | 18 + .../app/pages/night_actions/night_actions.rs | 327 ++++++++++++++++++ .../src/app/pages/night_actions/result.rs | 80 +++++ .../pages/night_actions/role/adjudicator.rs | 54 +++ .../pages/night_actions/role/alpha_wolf.rs | 31 ++ .../app/pages/night_actions/role/arcanist.rs | 63 ++++ .../app/pages/night_actions/role/beholder.rs | 75 ++++ .../pages/night_actions/role/bloodletter.rs | 34 ++ .../app/pages/night_actions/role/direwolf.rs | 30 ++ .../src/app/pages/night_actions/role/elder.rs | 43 +++ .../app/pages/night_actions/role/empath.rs | 55 +++ .../pages/night_actions/role/gravedigger.rs | 64 ++++ .../app/pages/night_actions/role/guardian.rs | 93 +++++ .../app/pages/night_actions/role/hunter.rs | 33 ++ .../app/pages/night_actions/role/insomniac.rs | 54 +++ .../app/pages/night_actions/role/lone_wolf.rs | 31 ++ .../pages/night_actions/role/maple_wolf.rs | 55 +++ .../src/app/pages/night_actions/role/mason.rs | 81 +++++ .../app/pages/night_actions/role/militia.rs | 34 ++ .../app/pages/night_actions/role/mortician.rs | 63 ++++ .../pages/night_actions/role/power_seer.rs | 54 +++ .../app/pages/night_actions/role/protector.rs | 30 ++ .../pages/night_actions/role/pyremaster.rs | 34 ++ .../src/app/pages/night_actions/role/seer.rs | 92 +++++ .../pages/night_actions/role/vindicator.rs | 31 ++ .../app/pages/night_actions/role_change.rs | 43 +++ .../src/app/pages/night_actions/roleblock.rs | 30 ++ .../app/pages/night_actions/shift_failed.rs | 30 ++ .../src/app/pages/night_actions/wolves.rs | 53 +++ werewolves/src/app/pages/not_found.rs | 2 +- werewolves/src/server/game.rs | 4 + werewolves/src/server/host.rs | 23 +- werewolves/src/server/runner.rs | 7 + 53 files changed, 2240 insertions(+), 56 deletions(-) create mode 100644 style/night.scss create mode 100644 werewolves/src/app/components/cover.rs create mode 100644 werewolves/src/app/components/role.rs create mode 100644 werewolves/src/app/pages/night_actions/damned.rs create mode 100644 werewolves/src/app/pages/night_actions/drunk_page.rs create mode 100644 werewolves/src/app/pages/night_actions/night_actions.rs create mode 100644 werewolves/src/app/pages/night_actions/result.rs create mode 100644 werewolves/src/app/pages/night_actions/role/adjudicator.rs create mode 100644 werewolves/src/app/pages/night_actions/role/alpha_wolf.rs create mode 100644 werewolves/src/app/pages/night_actions/role/arcanist.rs create mode 100644 werewolves/src/app/pages/night_actions/role/beholder.rs create mode 100644 werewolves/src/app/pages/night_actions/role/bloodletter.rs create mode 100644 werewolves/src/app/pages/night_actions/role/direwolf.rs create mode 100644 werewolves/src/app/pages/night_actions/role/elder.rs create mode 100644 werewolves/src/app/pages/night_actions/role/empath.rs create mode 100644 werewolves/src/app/pages/night_actions/role/gravedigger.rs create mode 100644 werewolves/src/app/pages/night_actions/role/guardian.rs create mode 100644 werewolves/src/app/pages/night_actions/role/hunter.rs create mode 100644 werewolves/src/app/pages/night_actions/role/insomniac.rs create mode 100644 werewolves/src/app/pages/night_actions/role/lone_wolf.rs create mode 100644 werewolves/src/app/pages/night_actions/role/maple_wolf.rs create mode 100644 werewolves/src/app/pages/night_actions/role/mason.rs create mode 100644 werewolves/src/app/pages/night_actions/role/militia.rs create mode 100644 werewolves/src/app/pages/night_actions/role/mortician.rs create mode 100644 werewolves/src/app/pages/night_actions/role/power_seer.rs create mode 100644 werewolves/src/app/pages/night_actions/role/protector.rs create mode 100644 werewolves/src/app/pages/night_actions/role/pyremaster.rs create mode 100644 werewolves/src/app/pages/night_actions/role/seer.rs create mode 100644 werewolves/src/app/pages/night_actions/role/vindicator.rs create mode 100644 werewolves/src/app/pages/night_actions/role_change.rs create mode 100644 werewolves/src/app/pages/night_actions/roleblock.rs create mode 100644 werewolves/src/app/pages/night_actions/shift_failed.rs create mode 100644 werewolves/src/app/pages/night_actions/wolves.rs diff --git a/style/big-screen.scss b/style/big-screen.scss index 7ce23df..3054830 100644 --- a/style/big-screen.scss +++ b/style/big-screen.scss @@ -7,10 +7,20 @@ left: 0; user-select: none; + font-size: 3em; + + .target-picker { + font-size: 1.25em; + + .target { + flex-grow: 1; + } + } + .role-reveal { width: 100%; height: 100%; - font-size: 2em; + // font-size: 2em; display: flex; flex-direction: row; flex-wrap: wrap; diff --git a/style/icon.scss b/style/icon.scss index 60dc632..9c3cee4 100644 --- a/style/icon.scss +++ b/style/icon.scss @@ -1,5 +1,9 @@ .icon-fit { - height: 1em; + // height: 1em; + flex-grow: 1; + flex-shrink: 1; + + padding: 1ch; } .icon { diff --git a/style/main.scss b/style/main.scss index 2abd134..a2841b2 100644 --- a/style/main.scss +++ b/style/main.scss @@ -53,10 +53,13 @@ $starts_as_villager_color_faint: color.change($starts_as_villager_color, $alpha: $damned_color_faint: color.change($damned_color, $alpha: 0.1); $drunk_color_faint: color.change($drunk_color, $alpha: 0.1); +$wakes_color: oklch(0.9195 0.1839 109.60356514768961); + @import 'faction'; @import 'setup'; @import 'icon'; @import 'big-screen'; +@import 'night'; @mixin flexbox() { display: -webkit-box; @@ -918,3 +921,17 @@ form { margin-bottom: 10px; } } + + +.role-title-span { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + text-align: center; + + margin: 0 1ch 0 1ch; + padding: 0.25ch 1ch 0.25ch 1ch; + gap: 1ch; + font-size: 1.25em; +} diff --git a/style/night.scss b/style/night.scss new file mode 100644 index 0000000..d0f5d94 --- /dev/null +++ b/style/night.scss @@ -0,0 +1,195 @@ +.cover-of-darkness { + font-size: 3em; + position: fixed; + top: 0; + left: 0; + height: 100vh; + width: 100vw; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; + text-wrap: wrap; + + p { + padding: 3ch; + } + + & button { + width: fit-content; + text-align: center; + align-self: center; + } +} + +.wolves-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.information { + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + gap: 0.5ch; + + font-size: 1.75em; + height: 100%; + + .subtext { + font-size: 1.5rem; + } + + .arcanist-targets { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 1ch; + font-size: 0.7em; + align-items: center; + + .and { + font-style: italic; + opacity: 50%; + font-size: 0.7em; + } + } +} + +.role-page { + padding: 1vh 1vw 1vh 1vw; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + gap: 1ch; + height: 98%; + + .title { + font-size: 2em; + font-weight: bold; + display: block; + } + + + .character { + padding: 1ch; + } +} + +.yellow { + color: $wakes_color; +} + +.wolves-list { + padding: 1ch; + display: flex; + flex-direction: row; + flex-wrap: wrap; + height: 100%; + align-items: center; + justify-content: space-around; + + .character { + display: flex; + flex-direction: column; + align-items: center; + min-width: 37vw; + + font-size: 1.5em; + + .role { + font-size: 1.25em; + font-weight: bold; + } + } +} + +.continue-button { + font-size: 2.25em; + padding: 0.3ch; + margin: 1ch; +} + + +.breakable { + word-wrap: normal; +} + +.inline-icons { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 0.5ch; + width: 100%; + align-items: center; + justify-content: center; +} + + +.target-picker { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + height: 100%; + font-size: 2em; + + .target { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + background-color: color.change($red1, $alpha: 0.1); + border: 1px solid color.change($red1, $alpha: 0.6); + + &.marked { + background-color: color.change($blue1, $alpha: 0.3); + border: 1px solid $blue1; + } + } +} + +.seer-icons, +.arcanist-icons { + display: flex; + flex-direction: row; + justify-content: space-between; + flex-grow: 1; + flex-shrink: 1; + gap: 10%; +} + +.two-column { + display: grid; + grid-template-columns: 3fr 2fr; + height: 100%; +} + +.seer-check { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + height: 100%; + align-items: center; + justify-content: space-around; +} + +.false-positives { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + font-weight: bold; + font-size: 0.5em; + + gap: 10px; +} + +.bottom-bound { + display: flex; + flex-direction: column; + justify-content: center; + gap: 10px; +} diff --git a/style/setup.scss b/style/setup.scss index f8ef41a..35d76d4 100644 --- a/style/setup.scss +++ b/style/setup.scss @@ -68,7 +68,6 @@ } .wakes { - $wakes_color: oklch(0.9195 0.1839 109.60356514768961); border: 2px solid $wakes_color; box-shadow: 0 0 3px $wakes_color; } diff --git a/werewolves-proto/src/message/night.rs b/werewolves-proto/src/message/night.rs index da565ac..aa3fcaf 100644 --- a/werewolves-proto/src/message/night.rs +++ b/werewolves-proto/src/message/night.rs @@ -252,7 +252,7 @@ impl ActionPrompt { | ActionPrompt::Insomniac { .. } => true, } } - pub(crate) const fn marked(&self) -> Option<(CharacterId, Option)> { + pub const fn marked(&self) -> Option<(CharacterId, Option)> { match self { ActionPrompt::Seer { marked, .. } | ActionPrompt::Protector { marked, .. } @@ -581,6 +581,46 @@ impl ActionPrompt { _ => false, } } + + #[rustfmt::skip] + pub fn targets(&self) -> Option<&[CharacterIdentity]> { + match self { + ActionPrompt::Seer { living_players: targets,.. } + | ActionPrompt::Protector { targets,.. } + | ActionPrompt::Arcanist { living_players: targets,.. } + | ActionPrompt::Gravedigger { dead_players: targets,.. } + | ActionPrompt::Hunter { living_players: targets,.. } + | ActionPrompt::Militia { living_players: targets,.. } + | ActionPrompt::MapleWolf { living_players: targets,.. } + | ActionPrompt::Guardian { living_players: targets,.. } + | ActionPrompt::Adjudicator { living_players: targets,.. } + | ActionPrompt::PowerSeer { living_players: targets,.. } + | ActionPrompt::Mortician { dead_players: targets,.. } + | ActionPrompt::BeholderChooses { living_players: targets,.. } + | ActionPrompt::MasonLeaderRecruit { potential_recruits: targets,.. } + | ActionPrompt::Empath { living_players: targets,.. } + | ActionPrompt::Vindicator { living_players: targets,.. } + | ActionPrompt::PyreMaster { living_players: targets,.. } + | ActionPrompt::WolfPackKill { living_villagers: targets,.. } + | ActionPrompt::AlphaWolf { living_villagers: targets,.. } + | ActionPrompt::DireWolf { living_players: targets,.. } + | ActionPrompt::LoneWolfKill { living_players: targets, .. } + | ActionPrompt::Bloodletter { + living_players: targets, + .. + } => Some(&**targets), + + ActionPrompt::WolvesIntro { .. } + | ActionPrompt::RoleChange { .. } + | ActionPrompt::Shapeshifter { .. } + | ActionPrompt::ElderReveal { .. } + | ActionPrompt::Insomniac { .. } + | ActionPrompt::CoverOfDarkness + | ActionPrompt::MasonsWake { .. } + | ActionPrompt::BeholderWakes { .. } + | ActionPrompt::DamnedIntro { .. } => None, + } + } } impl PartialOrd for ActionPrompt { diff --git a/werewolves/src/app.rs b/werewolves/src/app.rs index 130a71d..c8f45b1 100644 --- a/werewolves/src/app.rs +++ b/werewolves/src/app.rs @@ -1,5 +1,13 @@ pub mod pages { werewolves_macros::include_path!("werewolves/src/app/pages"); + + pub mod night_actions { + werewolves_macros::include_path!("werewolves/src/app/pages/night_actions"); + + pub mod role { + werewolves_macros::include_path!("werewolves/src/app/pages/night_actions/role"); + } + } } pub mod components { diff --git a/werewolves/src/app/components/cover.rs b/werewolves/src/app/components/cover.rs new file mode 100644 index 0000000..83bcd4f --- /dev/null +++ b/werewolves/src/app/components/cover.rs @@ -0,0 +1,30 @@ +use leptos::prelude::*; +use werewolves_proto::message::{host::HostNightMessage, night::ActionResponse}; + +#[component] +pub fn Cover( + #[prop(optional)] message: &'static str, + #[prop(optional)] reply: Option>>, + #[prop(default=HostNightMessage::ActionResponse(ActionResponse::Continue))] + reply_to_send: HostNightMessage, +) -> impl IntoView { + let message = if message.is_empty() { + "go to sleep" + } else { + message + }; + let next = move || { + reply.map(|reply| { + let reply_to_send = reply_to_send.clone(); + view! { } + }) + }; + move || { + view! { +
+

{message}

+ {next.clone()} +
+ } + } +} diff --git a/werewolves/src/app/components/identity.rs b/werewolves/src/app/components/identity.rs index d92f6bf..6a9e6a3 100644 --- a/werewolves/src/app/components/identity.rs +++ b/werewolves/src/app/components/identity.rs @@ -2,26 +2,20 @@ use leptos::prelude::*; use werewolves_proto::message::{Identification, PublicIdentity}; #[component] -pub fn IdentityInline(ident: ReadSignal) -> impl IntoView { - let number = move || { - ident - .read() - .number - .as_ref() - .map(|num| view! { {num.get()} }.into_any()) - .unwrap_or_else(|| { - view! { "?" } - .into_any() - }) - }; +pub fn IdentityInline(ident: PublicIdentity) -> impl IntoView { + let number = ident + .number + .as_ref() + .map(|num| view! { {num.get()} }.into_any()) + .unwrap_or_else(|| view! { "?" }.into_any()); let pronouns = move || { - ident.read().pronouns.as_ref().map(|p| { + ident.pronouns.as_ref().map(|p| { view! { "("{p.clone()}")" } }) }; view! { - {number} {move || ident.read().name.clone()} {pronouns} + {number} {move || ident.name.clone()} {pronouns} } } @@ -29,8 +23,7 @@ pub fn IdentityInline(ident: ReadSignal) -> impl IntoView { #[component] pub fn IdentificationInline(ident: Identification) -> impl IntoView { if !ident.public.name.trim().is_empty() { - return view! { } - .into_any(); + return view! { }.into_any(); } view! { diff --git a/werewolves/src/app/components/modal.rs b/werewolves/src/app/components/modal.rs index 01312b1..00a0db7 100644 --- a/werewolves/src/app/components/modal.rs +++ b/werewolves/src/app/components/modal.rs @@ -39,7 +39,7 @@ pub fn DialogModal( DialogMode::Close => view! {
{close}
}.into_any(), DialogMode::ConfirmOrClose(on_confirm) => view! {
- + {close}
} diff --git a/werewolves/src/app/components/role.rs b/werewolves/src/app/components/role.rs new file mode 100644 index 0000000..0ffa20a --- /dev/null +++ b/werewolves/src/app/components/role.rs @@ -0,0 +1,50 @@ +// 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 . + +use convert_case::{Case, Casing}; +use leptos::prelude::*; +use werewolves_proto::role::RoleTitle; + +use crate::app::{ + class::{AsClasses, Class}, + components::{AssociatedIcon, Icon, IconType, PartialAssociatedIcon}, +}; + +#[component] +pub fn RoleSpan(role: RoleTitle) -> impl IntoView { + let class = role.category().class(); + let icon = role + .icon() + .map(|icon| view! { }.into_any()) + .unwrap_or(view! {
}.into_any()); + let role_name = role.to_string().to_case(Case::Title); + view! { + {icon} {role_name} + } +} + +#[component] +pub fn RoleTitleSpan(role: RoleTitle) -> impl IntoView { + let class = role.category().class(); + let icon = role.icon().unwrap_or(role.alignment().icon()); + let text = role.to_string().to_case(Case::Title); + view! { + + + {text} + + } +} diff --git a/werewolves/src/app/components/sample.rs b/werewolves/src/app/components/sample.rs index fe7d8ec..f933f7e 100644 --- a/werewolves/src/app/components/sample.rs +++ b/werewolves/src/app/components/sample.rs @@ -83,5 +83,5 @@ pub fn Sample(children: Children) -> impl IntoView { #[component] pub fn Equals() -> impl IntoView { - view! { {"="} } + view! { = } } diff --git a/werewolves/src/app/error.rs b/werewolves/src/app/error.rs index 71d36fa..493c880 100644 --- a/werewolves/src/app/error.rs +++ b/werewolves/src/app/error.rs @@ -17,4 +17,6 @@ pub enum WolfError { PasswordConfirmNoMatch, #[error("please set a seat number")] NoSeatNumber, + #[error("no targets?")] + NoTargets, } diff --git a/werewolves/src/app/pages/game/big.rs b/werewolves/src/app/pages/game/big.rs index 8c501ba..a19e91a 100644 --- a/werewolves/src/app/pages/game/big.rs +++ b/werewolves/src/app/pages/game/big.rs @@ -5,7 +5,11 @@ use crate::{ ConsoleLogError, app::{ error::WolfError, - pages::{NotFound, game::host::RoleRevealCharacter}, + pages::{ + NotFound, + game::host::RoleRevealCharacter, + night_actions::{RolePrompt, RoleResult}, + }, storage::user::AuthContextStoreFields, }, }; @@ -19,18 +23,30 @@ use leptos_use::{ use reactive_stores::Store; use werewolves_proto::{ game::GameId, - message::{IntoClientResponse, WrappedServerMessage, host::ServerToHostMessage}, + message::{ + CharacterIdentity, IntoClientResponse, WrappedServerMessage, + host::ServerToHostMessage, + night::{ActionPrompt, ActionResult}, + }, }; use crate::app::storage::user::AuthContext; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, PartialEq, Default)] enum BigScreenPage { #[default] None, Setup, RoleReveal, QrCode, + ActionPrompt { + prompt: ActionPrompt, + page: usize, + }, + ActionResult { + character: Option, + result: ActionResult, + }, } #[component] @@ -155,8 +171,15 @@ pub fn BigScreen() -> impl IntoView { settings, } => todo!(), ServerToHostMessage::PlayerStates(_) => {} - ServerToHostMessage::ActionPrompt(action_prompt, _) => todo!(), - ServerToHostMessage::ActionResult(character_identity, action_result) => todo!(), + ServerToHostMessage::ActionPrompt(act, ppage) => { + page.set(BigScreenPage::ActionPrompt { + prompt: act, + page: ppage, + }); + } + ServerToHostMessage::ActionResult(character, result) => { + page.set(BigScreenPage::ActionResult { character, result }) + } ServerToHostMessage::Lobby { players: p, settings: s, @@ -199,9 +222,15 @@ pub fn BigScreen() -> impl IntoView { BigScreenPage::Setup => { view! { }.into_any() } - BigScreenPage::RoleReveal => view! { } - .into_any(), + BigScreenPage::RoleReveal => { + view! { }.into_any() + } BigScreenPage::QrCode => view! { }.into_any(), + BigScreenPage::ActionPrompt { prompt, page } => { + view! { }.into_any() + } + BigScreenPage::ActionResult { character, result } => view! { } + .into_any(), }; view! {
{content}
}.into_any() diff --git a/werewolves/src/app/pages/game/big/role_reveal.rs b/werewolves/src/app/pages/game/big/role_reveal.rs index 865c2ba..35b586f 100644 --- a/werewolves/src/app/pages/game/big/role_reveal.rs +++ b/werewolves/src/app/pages/game/big/role_reveal.rs @@ -9,10 +9,9 @@ pub fn BigScreenRoleReveal(acks: ReadSignal>) -> impl .into_iter() .map(|ackd| { let RoleRevealCharacter { char, acknowledged } = ackd; - let ident = RwSignal::new(char.into_public()).read_only(); view! {
- +
} }) diff --git a/werewolves/src/app/pages/game/big/setup.rs b/werewolves/src/app/pages/game/big/setup.rs index 345e3fd..cf5611e 100644 --- a/werewolves/src/app/pages/game/big/setup.rs +++ b/werewolves/src/app/pages/game/big/setup.rs @@ -17,7 +17,7 @@ pub fn QrView(game_id: GameId) -> impl IntoView {
qr code to join
-

{"scan the qrcode to join"}

+

scan the qrcode to join

} @@ -74,7 +74,7 @@ pub fn SetupView(settings: ReadSignal) -> impl IntoView { {categories}
{power_roles_count} - {"Power roles from..."} + Power roles from...
diff --git a/werewolves/src/app/pages/game/host.rs b/werewolves/src/app/pages/game/host.rs index a239296..c6bc66f 100644 --- a/werewolves/src/app/pages/game/host.rs +++ b/werewolves/src/app/pages/game/host.rs @@ -6,20 +6,36 @@ use leptos::prelude::*; use werewolves_proto::{ game::{Category, GameSettings}, message::{ - PlayerState, - host::{HostLobbyMessage, HostMessage, ServerToHostMessage as Srv2Host}, + CharacterIdentity, PlayerState, + host::{ + HostGameMessage, HostLobbyMessage, HostMessage, HostNightMessage, + ServerToHostMessage as Srv2Host, + }, + night::{ActionPrompt, ActionResult}, }, }; -use crate::app::{Preferences, components::DialogModal}; +use crate::app::{ + Preferences, + components::DialogModal, + pages::night_actions::{RolePrompt, RoleResult}, +}; use crate::{ConsoleLogError, app::error::WolfError}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, PartialEq, Default)] enum HostPage { #[default] None, Settings, RoleRevealAcks, + ActionPrompt { + prompt: ActionPrompt, + page: usize, + }, + ActionResult { + character: Option, + result: ActionResult, + }, } #[component] @@ -93,6 +109,15 @@ pub fn HostGamePage( acks.set(reveals); page.set(HostPage::RoleRevealAcks); } + Srv2Host::ActionPrompt(prompt, prompt_page) => { + page.set(HostPage::ActionPrompt { + prompt, + page: prompt_page, + }); + } + Srv2Host::ActionResult(character, result) => { + page.set(HostPage::ActionResult { character, result }) + } _ => log::error!("{message:#?}"), } } @@ -120,6 +145,26 @@ pub fn HostGamePage( HostPage::RoleRevealAcks => { view! { }.into_any() } + HostPage::ActionPrompt { prompt, page } => { + let reply_prompt = RwSignal::new(None); + Effect::new(move || { + if let Some(r) = reply_prompt.get() { + reply.set(Some(HostMessage::InGame(HostGameMessage::Night(r)))) + } + }); + view! { } + .into_any() + } + HostPage::ActionResult { character, result } => { + let reply_result = RwSignal::new(None); + Effect::new(move || { + if let Some(r) = reply_result.get() { + reply.set(Some(HostMessage::InGame(HostGameMessage::Night(r)))) + } + }); + view! { } + .into_any() + } }; view! { {cancel} diff --git a/werewolves/src/app/pages/game/host/role_reveal_acks.rs b/werewolves/src/app/pages/game/host/role_reveal_acks.rs index c803f84..78e25e5 100644 --- a/werewolves/src/app/pages/game/host/role_reveal_acks.rs +++ b/werewolves/src/app/pages/game/host/role_reveal_acks.rs @@ -23,14 +23,13 @@ pub fn RoleRevealAcks( .map(|ackd| { let RoleRevealCharacter { char, acknowledged } = ackd; let char_id = char.character_id; - let ident = RwSignal::new(char.into_public()).read_only(); let force_ready = move |ev: MouseEvent| { ev.prevent_default(); reply.set(Some(HostMessage::ForceRoleAckFor(char_id))); }; view! {
- + diff --git a/werewolves/src/app/pages/game/host/settings.rs b/werewolves/src/app/pages/game/host/settings.rs index 869187f..476a205 100644 --- a/werewolves/src/app/pages/game/host/settings.rs +++ b/werewolves/src/app/pages/game/host/settings.rs @@ -236,15 +236,12 @@ fn SettingsSetupSlot( .iter() .find(|p| p.identification.player_id == a) { - Some(player) => { - let ident = RwSignal::new(player.identification.public.clone()); - view! { - - - - } - .into_any() + Some(player) => view! { + + + } + .into_any(), None => view! { "assigned player not in lobby" } .into_any(), } @@ -335,7 +332,7 @@ fn SlotSettingsDialogBody( .cloned() }) .map(|p| p.identification.public) - .map(|id| view! { }.into_any()) + .map(|id| view! { }.into_any()) .unwrap_or_else(|| view! { "none" }.into_any()); let remove = move |ev: MouseEvent| { ev.prevent_default(); @@ -429,7 +426,6 @@ fn AssignmentSelection( .as_ref() .map(|assigned_id| p.identification.player_id == *assigned_id) .unwrap_or_default(); - let ident = RwSignal::new(p.identification.public.clone()); let pid = p.identification.player_id; let assign = move |ev: MouseEvent| { ev.prevent_default(); @@ -438,7 +434,7 @@ fn AssignmentSelection( view! { } }) diff --git a/werewolves/src/app/pages/night_actions/damned.rs b/werewolves/src/app/pages/night_actions/damned.rs new file mode 100644 index 0000000..404dd17 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/damned.rs @@ -0,0 +1,29 @@ +use leptos::prelude::*; + +use crate::app::components::{Icon, IconSource}; + +#[component] +pub fn DamnedIntroPage1() -> impl IntoView { + view! { +
+ "DAMNED" +
+ "YOU ARE DAMNED" + +
+
+ } +} + +#[component] +pub fn DamnedIntroPage2() -> impl IntoView { + view! { +
+ "DAMNED" +
+ "YOU RETAIN YOUR ROLE AND WIN IF EVIL WINS" + "HOWEVER, YOU CONTRIBUTE TO VILLAGE PARITY" +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/drunk_page.rs b/werewolves/src/app/pages/night_actions/drunk_page.rs new file mode 100644 index 0000000..428e254 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/drunk_page.rs @@ -0,0 +1,18 @@ +use leptos::prelude::*; +use werewolves_proto::aura::AuraTitle; + +use crate::app::components::{Icon, IconSource, PartialAssociatedIcon}; + +#[component] +pub fn DrunkPage() -> impl IntoView { + let icon = AuraTitle::Drunk.icon().unwrap_or(IconSource::Roleblock); + view! { +
+ "DRUNK" +
+ "YOU GOT DRUNK INSTEAD" + "YOUR NIGHT ACTION DID NOT TAKE PLACE" +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/night_actions.rs b/werewolves/src/app/pages/night_actions/night_actions.rs new file mode 100644 index 0000000..8ec1cb0 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/night_actions.rs @@ -0,0 +1,327 @@ +use leptos::prelude::*; +use werewolves_proto::{ + character::CharacterId, + message::{ + CharacterIdentity, + host::HostNightMessage, + night::{ActionPrompt, ActionResponse}, + }, + role::PreviousGuardianAction, +}; + +use crate::app::{ + class::AsClasses, + components::{Cover, IdentityInline}, + error::WolfError, + pages::night_actions::{ + DamnedIntroPage1, DamnedIntroPage2, RoleChange, WolfPackKill, WolvesIntro, + role::{ + AdjudicatorPage1, AlphaWolfPage1, ArcanistPage1, BeholderPage1, BeholderWakePage1, + BloodletterPage1, DirewolfPage1, ElderPage1, ElderPage2, EmpathPage1, GravediggerPage1, + GuardianPageNoPrevProtect, GuardianPagePreviousGuard, GuardianPagePreviousProtect1, + GuardianPagePreviousProtect2, HunterPage1, InsomniacPage1, LoneWolfPage1, + MapleWolfPage1, MasonRecruitPage1, MasonRecruitPage2, MasonsWake, MilitiaPage1, + MorticianPage1, PowerSeerPage1, ProtectorPage1, PyremasterPage1, SeerPage1, + VindicatorPage1, + }, + }, +}; + +pub trait RolePage { + fn role_pages(&self, big_screen: bool) -> ViewFn; +} + +#[component] +pub fn RolePrompt( + prompt: ActionPrompt, + page: usize, + #[prop(optional)] reply: Option>>, + #[prop(optional)] error: Option>>, +) -> impl IntoView { + let ident = move |character_id: CharacterIdentity| { + reply.map(|_| { + view! { } + }) + }; + let interactive = prompt.interactive(); + let targets = prompt.targets().map(|t| t.to_vec().into_boxed_slice()); + let marked = prompt + .marked() + .map(|t| [t.0].into_iter().chain(t.1)) + .into_iter() + .flatten() + .collect::>(); + let mut pages: Vec = match prompt { + ActionPrompt::CoverOfDarkness => vec![match reply { + Some(reply) => view! { }.into_any(), + None => view! { }.into_any(), + }], + ActionPrompt::ElderReveal { character_id } => vec![ + view! { + {ident(character_id.clone())} + + } + .into_any(), + view! { + {ident(character_id)} + + } + .into_any(), + ], + ActionPrompt::Adjudicator { character_id, .. } => vec![ + view! { + {ident(character_id)} + + } + .into_any(), + ], + ActionPrompt::RoleChange { + character_id, + new_role, + } => vec![ + view! { + {ident(character_id)} + + } + .into_any(), + ], + ActionPrompt::Seer { character_id, .. } => vec![ + view! { + {ident(character_id)} + + } + .into_any(), + ], + ActionPrompt::Protector { character_id, .. } => vec![ + view! { + {ident(character_id)} + + } + .into_any(), + ], + ActionPrompt::Arcanist { character_id, .. } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::Gravedigger { character_id, .. } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::Hunter { character_id, .. } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::Militia { character_id, .. } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::MapleWolf { + character_id, + nights_til_starvation, + .. + } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::Guardian { + character_id, + previous: None, + .. + } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::Guardian { + character_id, + previous: Some(PreviousGuardianAction::Protect(previous)), + .. + } => { + vec![ + view! { {ident(character_id.clone())} } + .into_any(), + view! { {ident(character_id)} } + .into_any(), + ] + } + ActionPrompt::Guardian { + character_id, + previous: Some(PreviousGuardianAction::Guard(previous)), + .. + } => { + vec![ + view! { {ident(character_id)} } + .into_any(), + ] + } + ActionPrompt::PowerSeer { character_id, .. } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::Mortician { character_id, .. } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::BeholderChooses { character_id, .. } => vec![ + view! { + {ident(character_id)} + + } + .into_any(), + ], + ActionPrompt::MasonLeaderRecruit { + character_id, + recruits_left, + .. + } => { + vec![ + view! { {ident(character_id.clone())} } + .into_any(), + view! { {ident(character_id)} }.into_any(), + ] + } + ActionPrompt::Empath { character_id, .. } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::Vindicator { character_id, .. } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::PyreMaster { character_id, .. } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::Shapeshifter { .. } => { + vec![] + } + ActionPrompt::AlphaWolf { character_id, .. } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::DireWolf { character_id, .. } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::LoneWolfKill { character_id, .. } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::Insomniac { character_id, .. } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::Bloodletter { character_id, .. } => { + vec![view! { {ident(character_id)} }.into_any()] + } + ActionPrompt::DamnedIntro { character_id, .. } => vec![ + view! { + {ident(character_id.clone())} + + } + .into_any(), + view! { + {ident(character_id)} + + } + .into_any(), + ], + ActionPrompt::BeholderWakes { character_id, .. } => vec![ + view! { + {ident(character_id)} + + } + .into_any(), + ], + ActionPrompt::WolfPackKill { .. } => vec![view! { }.into_any()], + ActionPrompt::MasonsWake { leader, masons } => { + vec![view! { }.into_any()] + } + ActionPrompt::WolvesIntro { wolves } => { + vec![view! { }.into_any()] + } + }; + // isn't it great that AnyView isn't Clone???? + if let Some(page) = pages.get(page).is_some().then(|| { + let p = pages.swap_remove(page); + let next_btn = reply.map(|reply| { + let next = move |_| { + reply.set(Some(if pages.get(page + 1).is_none() && !interactive { + HostNightMessage::ActionResponse(ActionResponse::ContinueToResult) + } else { + HostNightMessage::NextPage + })); + }; + view! { + + } + }); + view! { + {p} + {next_btn} + } + }) { + return page.into_any(); + } + let target_picker = match targets { + Some(targets) => { + let pick = RwSignal::new(None); + match reply { + Some(reply) => { + Effect::new(move || { + if let Some(target) = pick.get() { + reply.set(Some(HostNightMessage::ActionResponse( + ActionResponse::MarkTarget(target), + ))); + } + }); + view! { } + .into_any() + } + None => view! { }.into_any(), + } + } + None => { + if let Some(error) = error { + error.set(Some(WolfError::NoTargets)); + } else { + log::error!("no targets?"); + } + ().into_any() + } + }; + let continue_btn = reply.map(|reply| { + view! { + + } + }); + view! { + {target_picker} + {continue_btn} + } + .into_any() +} + +#[component] +pub fn TargetPicker( + targets: Box<[CharacterIdentity]>, + #[allow(clippy::boxed_local)] marked: Box<[CharacterId]>, + #[prop(optional)] pick: Option>>, +) -> impl IntoView { + let targets = targets + .into_iter() + .map(|target| { + let marked = marked.contains(&target.character_id); + let char_id = target.character_id; + let pick = move |_| { + if let Some(pick) = pick { + pick.set(Some(char_id)); + } + }; + view! { + + } + }) + .collect_view(); + + view! {
{targets}
} +} diff --git a/werewolves/src/app/pages/night_actions/result.rs b/werewolves/src/app/pages/night_actions/result.rs new file mode 100644 index 0000000..2b6df82 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/result.rs @@ -0,0 +1,80 @@ +use leptos::prelude::*; +use werewolves_proto::message::{ + CharacterIdentity, + host::HostNightMessage, + night::{ActionResponse, ActionResult}, +}; + +use crate::app::{ + components::Cover, + pages::night_actions::{ + DrunkPage, RoleblockPage, + role::{ + AdjudicatorResult, ArcanistResult, EmpathResult, GravediggerResultPage, + InsomniacResult, MorticianResultPage, PowerSeerResult, SeerResult, + }, + }, +}; + +#[component] +pub fn RoleResult( + character: Option, + result: ActionResult, + #[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::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::BeholderSawNothing => todo!(), + ActionResult::BeholderSawEverything => todo!(), + ActionResult::GoBackToSleep => return match reply { + Some(reply) => view! { } + .into_any(), + None => view! { }.into_any(), + }, + ActionResult::ShiftFailed => todo!(), + ActionResult::Continue => { + Effect::new(move || { + let Some(reply) = reply else { + return; + }; + reply.set(Some(HostNightMessage::ActionResponse( + ActionResponse::ContinueToResult, + ))); + }); + ().into_any() + } + ActionResult::SkippedByHost => todo!(), + }; + let next_btn = reply.map(|reply| { + view! { + + } + }); + + view! { + {body} + {next_btn} + } + .into_any() +} diff --git a/werewolves/src/app/pages/night_actions/role/adjudicator.rs b/werewolves/src/app/pages/night_actions/role/adjudicator.rs new file mode 100644 index 0000000..1d03b46 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/adjudicator.rs @@ -0,0 +1,54 @@ +// 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 leptos::prelude::*; +use werewolves_proto::{message::PublicIdentity, role::Killer}; + +use crate::app::components::{Icon, IconSource, IconType, IdentityInline}; + +#[component] +pub fn AdjudicatorPage1() -> impl IntoView { + view! { +
+ "ADJUDICATOR" +
+

"PICK A PLAYER"

+ +

"YOU WILL CHECK IF THEY APPEAR AS A KILLER"

+
+
+ } +} + +#[component] +pub fn AdjudicatorResult(killer: Killer, target: PublicIdentity) -> impl IntoView { + let text = match killer { + Killer::Killer => "IS A KILLER", + Killer::NotKiller => "IS NOT A KILLER", + }; + let icon = match killer { + Killer::Killer => view! { }, + Killer::NotKiller => view! { }, + }; + view! { +
+ "ADJUDICATOR" +
+ + {icon} +

{text}

+
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/alpha_wolf.rs b/werewolves/src/app/pages/night_actions/role/alpha_wolf.rs new file mode 100644 index 0000000..1c464c1 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/alpha_wolf.rs @@ -0,0 +1,31 @@ +// 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 leptos::prelude::*; + +#[component] +pub fn AlphaWolfPage1() -> impl IntoView { + view! { +
+ "ALPHA WOLF" +
+ + "IF YOU WISH TO USE YOUR " "ONCE PER GAME" + " KILL ABILITY" + + "POINT AT YOUR TARGET OR GO BACK TO SLEEP" +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/arcanist.rs b/werewolves/src/app/pages/night_actions/role/arcanist.rs new file mode 100644 index 0000000..0230527 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/arcanist.rs @@ -0,0 +1,63 @@ +// 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 leptos::prelude::*; +use werewolves_proto::{message::PublicIdentity, role::AlignmentEq}; + +use crate::app::components::{Icon, IconSource, IdentityInline}; + +#[component] +pub fn ArcanistPage1() -> impl IntoView { + view! { +
+ "ARCANIST" +
+ // space + "PICK TWO PLAYERS"
+ + +
"YOU WILL COMPARE THEIR ALIGNMENTS" +
+
+ } +} + +#[component] +pub fn ArcanistResult( + value: AlignmentEq, + targets: (PublicIdentity, PublicIdentity), +) -> impl IntoView { + let text = match value { + AlignmentEq::Same => "ARE THE SAME", + AlignmentEq::Different => "ARE DIFFERENT", + }; + let icons = match value { + AlignmentEq::Same => view! { }, + AlignmentEq::Different => view! { }, + }; + view! { +
+ "ARCANIST" +
+
+ + "AND" + +
+
{icons}
+ {text} +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/beholder.rs b/werewolves/src/app/pages/night_actions/role/beholder.rs new file mode 100644 index 0000000..3529dd1 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/beholder.rs @@ -0,0 +1,75 @@ +// 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 leptos::prelude::*; + +use crate::app::components::{Icon, IconSource}; + +#[component] +pub fn BeholderPage1() -> impl IntoView { + view! { +
+ "BEHOLDER" +
+ "PICK A PLAYER" + "YOU WILL SEE WHAT INFORMATION THEY MAY HAVE GATHERED" + "SHOULD THEY DIE TONIGHT" +
+
+ } +} + +#[component] +pub fn BeholderWakePage1() -> impl IntoView { + view! { +
+ "BEHOLDER" +
+ "YOUR TARGET HAS DIED" + "THIS IS THE LAST PIECE OF INFORMATION THEY SAW" +
+
+ } +} + +#[component] +pub fn BeholderSawNothing() -> impl IntoView { + view! { +
+ "BEHOLDER" +
+

"YOUR TARGET HAS DIED"

+ +

"BUT SAW NOTHING"

+
+
+ } +} + +#[component] +pub fn BeholderSawEverything() -> impl IntoView { + view! { +
+ "BEHOLDER" +
+ "YOUR TARGET HAS DIED" + + "BUT SAW " + "EVERYTHING" + + +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/bloodletter.rs b/werewolves/src/app/pages/night_actions/role/bloodletter.rs new file mode 100644 index 0000000..34697f2 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/bloodletter.rs @@ -0,0 +1,34 @@ +// 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 leptos::prelude::*; + +use crate::app::components::{Icon, IconSource}; + +#[component] +pub fn BloodletterPage1() -> impl IntoView { + view! { +
+ "BLOODLETTER" +
+ "PICK A PLAYER" + + "THEY'LL APPEAR AS A WOLF " "KILLER" + "AND POWERFUL" + "IN CHECKS FOR 2 NIGHTS" + +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/direwolf.rs b/werewolves/src/app/pages/night_actions/role/direwolf.rs new file mode 100644 index 0000000..c945b15 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/direwolf.rs @@ -0,0 +1,30 @@ +use leptos::IntoView; +// 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 leptos::prelude::*; + +#[component] +pub fn DirewolfPage1() -> impl IntoView { + view! { +
+ "DIREWOLF" +
+ "CHOOSE A TARGET" + "ANY VISITORS TO THIS TARGET WILL BE ROLE BLOCKED" + "YOU CANNOT CHOOSE YOURSELF OR THE SAME TARGET AS LAST NIGHT" +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/elder.rs b/werewolves/src/app/pages/night_actions/role/elder.rs new file mode 100644 index 0000000..dec4e83 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/elder.rs @@ -0,0 +1,43 @@ +// 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 leptos::prelude::*; + +#[component] +pub fn ElderPage1() -> impl IntoView { + view! { +
+ "ELDER" +
+ "YOU ARE THE ELDER" + + "IF YOU ARE EXECUTED BY THE VILLAGE FROM NOW ON " "ALL POWER ROLES WILL BE LOST" + +
+
+ } +} + +#[component] +pub fn ElderPage2() -> impl IntoView { + view! { +
+ "ELDER" +
+ "YOU STARTED THE GAME WITH PROTECTION FROM A NIGHT " + "DEATH — THIS MAY OR MAY NOT STILL BE INTACT" +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/empath.rs b/werewolves/src/app/pages/night_actions/role/empath.rs new file mode 100644 index 0000000..dc097e0 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/empath.rs @@ -0,0 +1,55 @@ +// 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 leptos::prelude::*; +use werewolves_proto::message::PublicIdentity; + +use crate::app::components::{Icon, IconSource, IdentityInline}; + +#[component] +pub fn EmpathPage1() -> impl IntoView { + view! { +
+ "EMPATH" +
+ "PICK A PLAYER" + "YOU WILL CHECK IF THEY ARE THE SCAPEGOAT" + "IF THEY ARE, YOU WILL TAKE ON THEIR CURSE" +
+
+ } +} + +#[component] +pub fn EmpathResult(scapegoat: bool, target: PublicIdentity) -> impl IntoView { + let text = match scapegoat { + true => "IS THE SCAPEGOAT", + false => "IS NOT THE SCAPEGOAT", + }; + let icon = match scapegoat { + true => IconSource::Scapegoat, + false => IconSource::RedX, + }; + view! { +
+ "EMPATH" +
+ + + {text} +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/gravedigger.rs b/werewolves/src/app/pages/night_actions/role/gravedigger.rs new file mode 100644 index 0000000..c35a2d4 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/gravedigger.rs @@ -0,0 +1,64 @@ +// 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 convert_case::{Case, Casing}; +use leptos::prelude::*; +use werewolves_proto::{message::PublicIdentity, role::RoleTitle}; + +use crate::app::components::{Icon, IconSource, IdentityInline, PartialAssociatedIcon}; + +#[component] +pub fn GravediggerPage1() -> impl IntoView { + view! { +
+ "GRAVEDIGGER" +
+ "PICK A " "DEAD" " PLAYER" + + "YOU WILL LEARN THEIR ROLE" +
+
+ } +} + +#[component] +pub fn GravediggerResultPage(role: Option, target: PublicIdentity) -> impl IntoView { + let text = role + .as_ref() + .map(|r| { + view! { + "WAS A " + {r.to_string().to_case(Case::Upper)} + } + .into_any() + }) + .unwrap_or_else(|| { + view! { "YOU FIND AN EMPTY GRAVE" } + .into_any() + }); + let icon = role + .as_ref() + .and_then(|i| i.icon()) + .unwrap_or(IconSource::Gravedigger); + view! { +
+ "GRAVEDIGGER" +
+ + + {text} +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/guardian.rs b/werewolves/src/app/pages/night_actions/role/guardian.rs new file mode 100644 index 0000000..c532d72 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/guardian.rs @@ -0,0 +1,93 @@ +// 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 leptos::prelude::*; +use werewolves_proto::message::CharacterIdentity; + +use crate::app::components::{Icon, IconSource, IdentityInline}; + +#[component] +pub fn GuardianPageNoPrevProtect() -> impl IntoView { + view! { +
+ "GUARDIAN" +
+ "PICK A PLAYER" + "CHOOSE SOMEONE TO PROTECT FROM DEATH" +
+
+ } +} + +#[component] +pub fn GuardianPagePreviousProtectSelf() -> impl IntoView { + view! { +
+ "GUARDIAN" +
+ "LAST TIME YOU PROTECTED YOURSELF" + "YOU CANNOT PROTECT YOURSELF AGAIN TONIGHT" +
+
+ } +} + +#[component] +pub fn GuardianPagePreviousProtect1(previous: CharacterIdentity) -> impl IntoView { + view! { +
+ "GUARDIAN" +
+ "LAST TIME YOU PROTECTED" +
+ +
+ "IF YOU PROTECT THEM AGAIN, YOU WILL INSTEAD GUARD THEM" +
+
+ } +} + +#[component] +pub fn GuardianPagePreviousProtect2(previous: CharacterIdentity) -> impl IntoView { + view! { +
+ "GUARDIAN" +
+ "LAST TIME YOU PROTECTED" +
+ +
+ + "IF ATTACKED WHILE GUARDED, YOU AND THEIR ATTACKER WILL INSTEAD DIE" + +
+
+ } +} + +#[component] +pub fn GuardianPagePreviousGuard(previous: CharacterIdentity) -> impl IntoView { + view! { +
+ "GUARDIAN" +
+ "LAST TIME YOU GUARDED" +
+ +
"YOU CANNOT PROTECT THEM TONIGHT" +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/hunter.rs b/werewolves/src/app/pages/night_actions/role/hunter.rs new file mode 100644 index 0000000..055b4f9 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/hunter.rs @@ -0,0 +1,33 @@ +// 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 leptos::prelude::*; + +use crate::app::components::{Icon, IconSource}; + +#[component] +pub fn HunterPage1() -> impl IntoView { + view! { +
+ HUNTER +
+ "SET A HUNTER'S TRAP ON A PLAYER" + + IF YOU DIE TONIGHT, OR ARE EXECUTED TOMORROW + THIS PLAYER WILL DIE AT NIGHT + +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/insomniac.rs b/werewolves/src/app/pages/night_actions/role/insomniac.rs new file mode 100644 index 0000000..5d48d1e --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/insomniac.rs @@ -0,0 +1,54 @@ +// 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 leptos::prelude::*; +use werewolves_proto::message::night::Visits; + +use crate::app::components::{Icon, IconSource}; + +#[component] +pub fn InsomniacPage1() -> impl IntoView { + view! { +
+ "INSOMNIAC" +
+ "YOUR SLEEP IS INTERRUPTED" + "YOU'VE NOTICED VISITORS IN THE NIGHT" +
+
+ } +} + +#[component] +pub fn InsomniacResult(visits: Visits) -> impl IntoView { + let visitors = visits + .iter() + .map(|visitor| { + view! { +
+ {visitor.number.get()} + {visitor.name.clone()} +
+ } + }) + .collect_view(); + view! { +
+ "INSOMNIAC" +
+ "YOU WERE VISITED IN THE NIGHT BY:"
{visitors}
+
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/lone_wolf.rs b/werewolves/src/app/pages/night_actions/role/lone_wolf.rs new file mode 100644 index 0000000..3ca9a38 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/lone_wolf.rs @@ -0,0 +1,31 @@ +// 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 leptos::prelude::*; + +#[component] +pub fn LoneWolfPage1() -> impl IntoView { + view! { +
+ LONE WOLF +
+ + YOU MUST KILL TONIGHT IN ANGER OVER A FELLOW + WOLF HAVING BEEN SLAIN + + PICK A PLAYER +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/maple_wolf.rs b/werewolves/src/app/pages/night_actions/role/maple_wolf.rs new file mode 100644 index 0000000..72febd8 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/maple_wolf.rs @@ -0,0 +1,55 @@ +// 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 leptos::prelude::*; + +use crate::app::components::{Icon, IconSource}; + +#[component] +pub fn MapleWolfPage1(nights_til_starvation: u8) -> impl IntoView { + let food_state = if nights_til_starvation == 0 { + view! { + "YOU ARE STARVING" + "IF YOU FAIL TO EAT TONIGHT, YOU WILL DIE" + } + .into_any() + } else { + let nights = if nights_til_starvation == 1 { + view! { "TOMORROW NIGHT " } + .into_any() + } else { + view! { + "IN " + {nights_til_starvation} + " NIGHTS " + } + .into_any() + }; + view! { + + "IF YOU FAIL TO EAT " {nights} "YOU WILL ""STARVE" + + } + .into_any() + }; + view! { +
+ "MAPLE WOLF" +
+ "YOU CAN CHOOSE TO EAT A PLAYER TONIGHT" + {food_state} +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/mason.rs b/werewolves/src/app/pages/night_actions/role/mason.rs new file mode 100644 index 0000000..50b359b --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/mason.rs @@ -0,0 +1,81 @@ +// 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; + +use leptos::prelude::*; +use werewolves_proto::message::CharacterIdentity; + +use crate::app::components::IdentityInline; + +#[component] +pub fn MasonRecruitPage1(recruits_left: NonZeroU8) -> impl IntoView { + let recruitments = match recruits_left.get() { + 0 => unreachable!(), + 1 => view! { + {1} + " RECRUITMENT" + }, + num => view! { + {num} + " RECRUITMENTS" + }, + }; + view! { +
+ "MASON LEADER" +
+ "YOU HAVE "{recruitments}" LEFT" + + "RECRUITS WILL WAKE WITH YOU EVERY NIGHT" + " WHILE THEY ARE ALIVE AND REMAIN VILLAGE ALIGNED" + +
+
+ } +} + +#[component] +pub fn MasonRecruitPage2() -> impl IntoView { + view! { +
+ "MASON LEADER" +
+ "WOULD YOU LIKE TO RECRUIT TONIGHT?" +
+
+ } +} + +#[component] +pub fn MasonsWake(leader: CharacterIdentity, masons: Box<[CharacterIdentity]>) -> impl IntoView { + let title = view! { + "MASONS OF " + {leader.name.clone()} + }; + let masons = masons + .iter() + .map(|mason| { + view! { } + }) + .collect_view(); + view! { +
+ {title} +
+ "THE MASONS CONVENE AT NIGHT"
{masons}
+
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/militia.rs b/werewolves/src/app/pages/night_actions/role/militia.rs new file mode 100644 index 0000000..75f760e --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/militia.rs @@ -0,0 +1,34 @@ +// 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 leptos::prelude::*; + +use crate::app::components::{Icon, IconSource}; + +#[component] +pub fn MilitiaPage1() -> impl IntoView { + view! { +
+ "MILITIA" +
+ + "IF YOU WISH TO USE YOUR " "ONCE PER GAME" + " KILL ABILITY" + + + "PICK A PLAYER " "OR GO BACK TO SLEEP" +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/mortician.rs b/werewolves/src/app/pages/night_actions/role/mortician.rs new file mode 100644 index 0000000..bfd8839 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/mortician.rs @@ -0,0 +1,63 @@ +// 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 leptos::prelude::*; +use werewolves_proto::{diedto::DiedToTitle, message::PublicIdentity}; + +use crate::app::components::{Icon, IconSource, IdentityInline, PartialAssociatedIcon}; + +#[component] +pub fn MorticianPage1() -> impl IntoView { + view! { +
+ "MORTICIAN" +
+ "PICK A ""DEAD"" PLAYER" + + "YOU WILL LEARN THE CAUSE " "OF THEIR DEATH" +
+
+ } +} + +#[component] +pub fn MorticianResultPage(died_to: DiedToTitle, target: PublicIdentity) -> impl IntoView { + let text = match died_to { + DiedToTitle::Execution => "Execution", + DiedToTitle::MapleWolf => "Maple Wolf", + DiedToTitle::MapleWolfStarved => "Starvation", + DiedToTitle::Militia => "Militia Shot", + DiedToTitle::Wolfpack => "Wolfpack", + DiedToTitle::AlphaWolf => "Alpha Wolf", + DiedToTitle::Shapeshift => "Shapeshifting", + DiedToTitle::Hunter => "Hunter Trap", + DiedToTitle::GuardianProtecting => "Guardian", + DiedToTitle::PyreMaster => "Pyre Master", + DiedToTitle::PyreMasterLynchMob => "An Angry Mob of Villagers Against Fire", + DiedToTitle::MasonLeaderRecruitFail => "Occupational Hazard (Mason Recruit Fail)", + DiedToTitle::LoneWolf => "Lone Wolf", + }; + let icon = died_to.icon().unwrap_or(IconSource::Mortician); + view! { +
+ "MORTICIAN" +
+ + "DIED TO" + + {text} +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/power_seer.rs b/werewolves/src/app/pages/night_actions/role/power_seer.rs new file mode 100644 index 0000000..6446a2d --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/power_seer.rs @@ -0,0 +1,54 @@ +// 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 leptos::prelude::*; +use werewolves_proto::{message::PublicIdentity, role::Powerful}; + +use crate::app::components::{Icon, IconSource, IdentityInline}; + +#[component] +pub fn PowerSeerPage1() -> impl IntoView { + view! { +
+ "POWER SEER" +
+ "PICK A PLAYER" + "YOU WILL CHECK IF THEY ARE POWERFUL" +
+
+ } +} + +#[component] +pub fn PowerSeerResult(powerful: Powerful, target: PublicIdentity) -> impl IntoView { + let text = match powerful { + Powerful::Powerful => "IS POWERFUL", + Powerful::NotPowerful => "IS NOT POWERFUL", + }; + let icon = match powerful { + Powerful::Powerful => IconSource::Powerful, + Powerful::NotPowerful => IconSource::RedX, + }; + view! { +
+ "POWER SEER" +
+ + + {text} +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/protector.rs b/werewolves/src/app/pages/night_actions/role/protector.rs new file mode 100644 index 0000000..2a51c71 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/protector.rs @@ -0,0 +1,30 @@ +// 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 leptos::prelude::*; + +use crate::app::components::{Icon, IconSource}; + +#[component] +pub fn ProtectorPage1() -> impl IntoView { + view! { +
+ "PROTECTOR" +
+ "PICK A PLAYER" + "YOU WILL PROTECT THEM FROM A DEATH TONIGHT" +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/pyremaster.rs b/werewolves/src/app/pages/night_actions/role/pyremaster.rs new file mode 100644 index 0000000..9ddd307 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/pyremaster.rs @@ -0,0 +1,34 @@ +// 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 leptos::prelude::*; + +use crate::app::components::{Icon, IconSource}; + +#[component] +pub fn PyremasterPage1() -> impl IntoView { + view! { +
+ "PYREMASTER" +
+ "YOU CAN CHOOSE TO THROW A PLAYER ON THE PYRE" + + + "IF YOU KILL " "TWO" " GOOD VILLAGERS LIKE THIS " + "YOU WILL DIE AS WELL" + +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/seer.rs b/werewolves/src/app/pages/night_actions/role/seer.rs new file mode 100644 index 0000000..dfbe787 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/seer.rs @@ -0,0 +1,92 @@ +// 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 leptos::prelude::*; +use werewolves_proto::{ + message::PublicIdentity, + role::{Alignment, RoleTitle}, +}; + +use crate::app::components::{AssociatedIcon, Icon, IconSource, IdentityInline, RoleTitleSpan}; + +#[component] +pub fn SeerPage1() -> impl IntoView { + view! { +
+ "SEER" +
+ "PICK A PLAYER"
+ + +
"YOU WILL CHECK THEIR ALIGNMENT" +
+
+ } +} + +#[component] +pub fn SeerResult(alignment: Alignment, target: PublicIdentity) -> impl IntoView { + let text = match alignment { + Alignment::Village => "VILLAGE", + Alignment::Wolves => "WOLFPACK", + Alignment::Damned => "DAMNED", + }; + let additional_info = match alignment { + Alignment::Village => view! { } + .into_any(), + Alignment::Wolves => view! { } + .into_any(), + Alignment::Damned => view! { +
+ "THIS PERSON IS " "DAMNED" "THEY WIN ALONGSIDE EVIL" +
+ } + .into_any(), + }; + view! { +
+ "SEER" +
+
+
+ + + {text} +
+ {additional_info} +
+
+
+ } +} + +#[component] +fn FalselyAppearsAs( + #[allow(clippy::boxed_local)] roles: Box<[RoleTitle]>, + alignment_text: &'static str, +) -> impl IntoView { + let false_positives = roles + .iter() + .copied() + .map(|role| { + view! { } + }) + .collect_view(); + view! { +
+ "ROLES THAT FALSELY APPEAR AS " {alignment_text} +
{false_positives}
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role/vindicator.rs b/werewolves/src/app/pages/night_actions/role/vindicator.rs new file mode 100644 index 0000000..dcc0896 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/vindicator.rs @@ -0,0 +1,31 @@ +// 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 leptos::prelude::*; + +use crate::app::components::{Icon, IconSource}; + +#[component] +pub fn VindicatorPage1() -> impl IntoView { + view! { +
+ "VINDICATOR" +
+ "A VILLAGER WAS EXECUTED" + + "PICK A PLAYER TO PROTECT FROM A DEATH TONIGHT" +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/role_change.rs b/werewolves/src/app/pages/night_actions/role_change.rs new file mode 100644 index 0000000..2394783 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role_change.rs @@ -0,0 +1,43 @@ +// 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 convert_case::{Case, Casing}; +use leptos::prelude::*; +use werewolves_proto::role::RoleTitle; + +use crate::app::{ + class::{AsClasses, Class}, + components::{Icon, PartialAssociatedIcon}, +}; + +#[component] +pub fn RoleChange(role: RoleTitle) -> impl IntoView { + let class = role.category().class(); + let icon = role.icon().map(|icon| { + view! { } + }); + view! { +
+ {"ROLE CHANGE"} +
+ {"YOUR ROLE HAS CHANGED"} {icon} + + {"YOUR NEW ROLE IS "} + {role.to_string().to_case(Case::Upper)} + +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/roleblock.rs b/werewolves/src/app/pages/night_actions/roleblock.rs new file mode 100644 index 0000000..3b7af7b --- /dev/null +++ b/werewolves/src/app/pages/night_actions/roleblock.rs @@ -0,0 +1,30 @@ +// 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 leptos::prelude::*; + +use crate::app::components::{Icon, IconSource}; + +#[component] +pub fn RoleblockPage() -> impl IntoView { + view! { +
+ "ROLE BLOCKED" +
+ "YOU WERE ROLE BLOCKED" + "YOUR NIGHT ACTION DID NOT TAKE PLACE" +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/shift_failed.rs b/werewolves/src/app/pages/night_actions/shift_failed.rs new file mode 100644 index 0000000..796107c --- /dev/null +++ b/werewolves/src/app/pages/night_actions/shift_failed.rs @@ -0,0 +1,30 @@ +// 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 leptos::prelude::*; + +use crate::app::components::{Icon, IconSource}; + +#[component] +pub fn ShiftFailed() -> impl IntoView { + view! { +
+ "SHIFT FAILED" +
+ "YOUR SHIFT HAS FAILED" + "YOU RETAIN YOUR SHAPESHIFT ABILITY" +
+
+ } +} diff --git a/werewolves/src/app/pages/night_actions/wolves.rs b/werewolves/src/app/pages/night_actions/wolves.rs new file mode 100644 index 0000000..4308cb9 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/wolves.rs @@ -0,0 +1,53 @@ +// 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 convert_case::{Case, Casing}; +use leptos::prelude::*; +use werewolves_proto::{message::CharacterIdentity, role::RoleTitle}; + +use crate::app::components::{Icon, IconSource, IdentityInline}; + +#[component] +pub fn WolvesIntro(wolves: Box<[(CharacterIdentity, RoleTitle)]>) -> impl IntoView { + let wolves = wolves + .into_iter() + .map(|w| { + view! { +
+ {w.1.to_string().to_case(Case::Title)} + +
+ } + }) + .collect_view(); + view! { +
+ "THESE ARE THE WOLVES" +
{wolves}
+
+ } +} + +#[component] +pub fn WolfPackKill() -> impl IntoView { + view! { +
+ "WOLF PACK KILL" +
+ "CHOOSE A TARGET TO EAT TONIGHT" + "WOLVES MUST BE UNANIMOUS" +
+
+ } +} diff --git a/werewolves/src/app/pages/not_found.rs b/werewolves/src/app/pages/not_found.rs index 9759170..e13825d 100644 --- a/werewolves/src/app/pages/not_found.rs +++ b/werewolves/src/app/pages/not_found.rs @@ -7,7 +7,7 @@ pub fn NotFound() -> impl IntoView { provide_meta_context(); view! { -

{"not found"}

+

not found

"specifically, this is the 404 page"

} } diff --git a/werewolves/src/server/game.rs b/werewolves/src/server/game.rs index dd231ed..00e4f45 100644 --- a/werewolves/src/server/game.rs +++ b/werewolves/src/server/game.rs @@ -44,6 +44,10 @@ impl<'a> GameRunner<'a> { self.game.game_over() } + pub fn get_game(&self) -> &Game { + &self.game + } + pub async fn process(&mut self, msg: HostOrClientMessage) -> Option { let start = Utc::now(); let msg = match msg { diff --git a/werewolves/src/server/host.rs b/werewolves/src/server/host.rs index a43087f..7decd4c 100644 --- a/werewolves/src/server/host.rs +++ b/werewolves/src/server/host.rs @@ -94,11 +94,24 @@ impl Host { } async fn send_message(&mut self, msg: ServerToHostMessage) -> Result<(), anyhow::Error> { - log::debug!( - "sending {} message to {}", - msg.title().to_string().bold(), - self.who.dimmed() - ); + match &msg { + ServerToHostMessage::ActionPrompt(prompt, _) => log::debug!( + "sending ActionPrompt({}) message to {}", + prompt.title().to_string().bold(), + self.who.dimmed() + ), + ServerToHostMessage::ActionResult(_, result) => log::debug!( + "sending ActionResult({}) message to {}", + result.title().to_string().bold(), + self.who.dimmed() + ), + _ => log::debug!( + "sending {} message to {}", + msg.title().to_string().bold(), + self.who.dimmed() + ), + } + Ok(self .socket .send(ws::Message::Binary( diff --git a/werewolves/src/server/runner.rs b/werewolves/src/server/runner.rs index 7c43474..895dc5f 100644 --- a/werewolves/src/server/runner.rs +++ b/werewolves/src/server/runner.rs @@ -200,6 +200,7 @@ pub async fn run_game( .await .is_some() { + log::info!("game over, man!"); game.game_state = GameRecordState::GameOver(current_game.story()); if let Err(err) = db.game().store_game_state(&game).await { log::error!("saving game({game_id}) state: {err}; quitting..."); @@ -207,6 +208,12 @@ pub async fn run_game( } break; } + game.game_state = GameRecordState::Started(runner.get_game().clone()); + log::info!("saving game state"); + if let Err(err) = db.game().store_game_state(&game).await { + log::error!("saving game({game_id}) state: {err}; quitting..."); + return; + } } } GameRecordState::GameOver(story) => {