From 5a452b220cfb95fa1accfd23cef21c1d58002ec2 Mon Sep 17 00:00:00 2001 From: emilis Date: Sun, 22 Feb 2026 23:15:03 +0000 Subject: [PATCH] overrides + arcanist fixes --- .gitignore | 1 + public/img/balanced-scales.svg | 67 + public/img/icons.svg | 5293 ----------------- public/img/unbalanced-scales.svg | 81 + style/icon.scss | 1 + style/main.scss | 140 + style/night.scss | 47 + werewolves/src/app/components/icon.rs | 2 + werewolves/src/app/components/incdec.rs | 96 + werewolves/src/app/components/nav.rs | 51 +- werewolves/src/app/pages/game.rs | 16 +- werewolves/src/app/pages/game/big.rs | 3 +- werewolves/src/app/pages/game/host.rs | 82 +- werewolves/src/app/pages/game/host/daytime.rs | 14 +- .../pages/game/host/overrides/overrides.rs | 452 ++ .../app/pages/game/host/overrides/prompt.rs | 378 ++ .../app/pages/game/host/overrides/result.rs | 312 + werewolves/src/app/pages/lobby.rs | 4 - .../app/pages/night_actions/night_actions.rs | 95 +- .../src/app/pages/night_actions/result.rs | 55 +- .../pages/night_actions/role/adjudicator.rs | 6 +- .../app/pages/night_actions/role/arcanist.rs | 12 +- .../app/pages/night_actions/role/beholder.rs | 4 +- .../pages/night_actions/role/bloodletter.rs | 12 +- .../pages/night_actions/role/shapeshifter.rs | 35 + 25 files changed, 1870 insertions(+), 5389 deletions(-) create mode 100644 public/img/balanced-scales.svg delete mode 100644 public/img/icons.svg create mode 100644 public/img/unbalanced-scales.svg create mode 100644 werewolves/src/app/components/incdec.rs create mode 100644 werewolves/src/app/pages/game/host/overrides/overrides.rs create mode 100644 werewolves/src/app/pages/game/host/overrides/prompt.rs create mode 100644 werewolves/src/app/pages/game/host/overrides/result.rs delete mode 100644 werewolves/src/app/pages/lobby.rs create mode 100644 werewolves/src/app/pages/night_actions/role/shapeshifter.rs diff --git a/.gitignore b/.gitignore index 4d6c313..54d09f9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ werewolves/img/icons.svg license_headers.fish util/ werewolves/Trunk-local.toml +public/img/icons.svg werewolves-old-client/ werewolves-old-server/ diff --git a/public/img/balanced-scales.svg b/public/img/balanced-scales.svg new file mode 100644 index 0000000..3628e96 --- /dev/null +++ b/public/img/balanced-scales.svg @@ -0,0 +1,67 @@ + + + + diff --git a/public/img/icons.svg b/public/img/icons.svg deleted file mode 100644 index 4a7f13e..0000000 --- a/public/img/icons.svg +++ /dev/null @@ -1,5293 +0,0 @@ - - - - diff --git a/public/img/unbalanced-scales.svg b/public/img/unbalanced-scales.svg new file mode 100644 index 0000000..a805756 --- /dev/null +++ b/public/img/unbalanced-scales.svg @@ -0,0 +1,81 @@ + + + + diff --git a/style/icon.scss b/style/icon.scss index 10272b5..ac4f126 100644 --- a/style/icon.scss +++ b/style/icon.scss @@ -17,4 +17,5 @@ .icon-shrink { flex-shrink: 1; + height: 1em; } diff --git a/style/main.scss b/style/main.scss index de63852..11106e0 100644 --- a/style/main.scss +++ b/style/main.scss @@ -185,6 +185,10 @@ nav.header { white-space: nowrap; align-items: center; + &[hidden] { + display: none; + } + font-size: 1.5em; .username { @@ -292,6 +296,23 @@ dialog::backdrop { } } +.host-in-game-wrapper { + z-index: 1; + background-color: black; + height: 100vh; + width: 100vw; + margin: 0; + position: fixed; + display: block; + top: 0; + left: 0; + + &>div, + &>nav { + padding: 1ch; + } +} + #change-password, #update-profile { .pwless-notice { @@ -999,6 +1020,10 @@ form { gap: 0.5ch; padding-bottom: 1ch; + max-height: 75vh; + overflow-y: scroll; + scrollbar-width: thin; + .character { flex-grow: 1; display: flex; @@ -1028,3 +1053,118 @@ form { } } } + +.overrides-screen { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + gap: 10px; + align-items: center; + + width: 80vw; + margin-left: 10vw; + margin-right: 10vw; + + .override-selector { + .category { + .selected { + background-color: rgba(0, 255, 0, 0.7); + border: 1px solid rgba(0, 255, 0, 1.0); + color: black; + } + + label { + font-size: 2em; + } + + button:not(:hover) { + color: white; + } + + gap: 10px; + + display: flex; + flex-direction: column; + flex-wrap: nowrap; + max-width: 80vw; + + .overrides-screens { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin: 0; + padding-left: 0; + + li { + margin: 0; + list-style: none; + } + } + } + } +} + + +.prompt-overrides, +.result-overrides { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-width: 40vw; + align-items: center; + + .close { + width: 100%; + } + + font-size: 2em; + + .result-number { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 20px; + } + + .result-options, + .prompt-options { + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-items: center; + gap: 5px; + margin-top: 10px; + + .option-set { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 5px; + } + + .result-option, + .prompt-option { + width: 100%; + padding: 5px; + border: 1px solid white; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + gap: 5px; + } + } + + .prompt-number { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + gap: 1ch; + + button { + padding-left: 0.5ch; + padding-right: 0.5ch; + } + } +} diff --git a/style/night.scss b/style/night.scss index 026a882..7d7595b 100644 --- a/style/night.scss +++ b/style/night.scss @@ -38,6 +38,8 @@ font-size: 1.75em; height: 100%; + max-height: 100%; + max-width: 100%; .subtext { font-size: 1.5rem; @@ -71,6 +73,9 @@ font-size: 2em; font-weight: bold; display: block; + max-width: 100%; + overflow: hidden; + min-height: 2ch; } @@ -123,17 +128,53 @@ flex-direction: row; flex-wrap: wrap; gap: 0.5ch; + row-gap: 0px; width: 100%; align-items: center; justify-content: center; + + .icon-fit { + padding: 0px; + } } +.bool-picker { + width: calc(100% - 6ch); + height: calc(100% - 6ch); + display: flex; + flex-direction: row; + flex-wrap: nowrap; + padding: 3ch; + gap: 3ch; + + &>button { + font-size: 3em; + width: 30vw; + flex-grow: 1; + + background-color: color.change($red1, $alpha: 0.1); + border: 1px solid color.change($red1, $alpha: 0.6); + + &:hover { + background-color: color.change($blue1, $alpha: 0.3); + border: 1px solid $blue1; + } + } +} + .target-picker { display: flex; flex-direction: row; flex-wrap: wrap; + &.allow-scroll { + max-height: 70vh; + overflow-y: scroll; + scrollbar-width: thin; + justify-content: unset; + } + height: 100%; font-size: 2em; @@ -161,6 +202,12 @@ flex-grow: 1; flex-shrink: 1; gap: 10%; + + @media only screen and (min-width : 1200px) { + &>img { + height: auto; + } + } } .two-column { diff --git a/werewolves/src/app/components/icon.rs b/werewolves/src/app/components/icon.rs index 5bb27d3..133d095 100644 --- a/werewolves/src/app/components/icon.rs +++ b/werewolves/src/app/components/icon.rs @@ -78,6 +78,8 @@ decl_icon!( Mason: "/img/mason.svg", NotEqual: "/img/not-equal.svg", Equal: "/img/equal.svg", + UnbalancedScales: "/img/unbalanced-scales.svg", + BalancedScales: "/img/balanced-scales.svg", RedX: "/img/red-x.svg", Damned: "/img/damned.svg", Bloodlet: "/img/bloodlet.svg", diff --git a/werewolves/src/app/components/incdec.rs b/werewolves/src/app/components/incdec.rs new file mode 100644 index 0000000..49c56a9 --- /dev/null +++ b/werewolves/src/app/components/incdec.rs @@ -0,0 +1,96 @@ +use core::ops::{Add, AddAssign, Not, RangeInclusive, SubAssign}; + +use leptos::prelude::*; + +#[component] +pub fn IncDecU8( + value: RwSignal, + #[prop(default = 0..=0xFFu8)] value_range: RangeInclusive, +) -> impl IntoView { + let dec_disabled = { + let value_range = value_range.clone(); + move || !value_range.contains(&value.get().saturating_sub(1)) + }; + view! { +
+ + {move || value.get()} + +
+ } +} + +pub trait Increment: Copy + AddAssign { + fn increment(self) -> Self; +} + +pub trait Decrement: Copy + SubAssign { + fn decrement(self) -> Self; +} + +macro_rules! inc_dec_impl { + ($($n:ty),*) => { + $( + impl Increment for $n { + fn increment(self) -> Self { + self.saturating_add(1) + } + } + impl Decrement for $n { + fn decrement(self) -> Self { + self.saturating_sub(1) + } + } + )* + }; +} +inc_dec_impl!( + u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize +); + +#[component] +pub fn IncDec( + value: RwSignal, + #[prop(default = V::default()..=V::default().not())] value_range: RangeInclusive, +) -> impl IntoView +where + V: Increment + + Decrement + + Eq + + Ord + + Not + + Default + + Send + + Sync + + ToString + + 'static, +{ + let dec_disabled = { + let value_range = value_range.clone(); + move || !value_range.contains(&value.get().decrement()) + }; + view! { +
+ + {move || value.get().to_string()} + +
+ } +} diff --git a/werewolves/src/app/components/nav.rs b/werewolves/src/app/components/nav.rs index affafc9..832bb55 100644 --- a/werewolves/src/app/components/nav.rs +++ b/werewolves/src/app/components/nav.rs @@ -4,11 +4,17 @@ use leptos::{ev::MouseEvent, prelude::*}; use leptos_router::hooks::use_url; use reactive_stores::Store; use uuid::Uuid; -use werewolves_proto::{error::ServerError, game::GameId, token::TokenString}; +use werewolves_proto::{ + error::ServerError, + game::GameId, + message::host::{HostGameMessage, HostMessage, HostNightMessage}, + token::TokenString, +}; use crate::{ app::{ components::LinkButton, + pages::host::HostPage, storage::user::{AuthContext, AuthContextStoreFields}, }, db::AppState, @@ -140,3 +146,46 @@ fn is_big_screen_path(path: &str) -> bool { && Uuid::parse_str(parts[1]).is_ok() && parts[2] == "big" } + +#[component] +pub fn HostInGameNav( + reply: WriteSignal>, + night: ReadSignal, + show_back_button_only: RwSignal, + overrides_set: RwSignal, +) -> impl IntoView { + let previous = move |_| reply.set(Some(HostMessage::InGame(HostGameMessage::PreviousState))); + let skip = move |_| { + reply.set(Some(HostMessage::InGame(HostGameMessage::Night( + HostNightMessage::SkipAction, + )))) + }; + let back = move |_| { + show_back_button_only.set(false); + overrides_set.set(false); + reply.set(Some(HostMessage::GetState)); + }; + let hide_if_not_back_button = move || show_back_button_only.get(); + let hide_if_not_night = move || show_back_button_only.get() || !night.get(); + view! { + + } +} diff --git a/werewolves/src/app/pages/game.rs b/werewolves/src/app/pages/game.rs index d9e96ae..774ecd6 100644 --- a/werewolves/src/app/pages/game.rs +++ b/werewolves/src/app/pages/game.rs @@ -1,5 +1,5 @@ pub mod big; -mod host; +pub mod host; mod player; use codee::binary::MsgpackSerdeCodec; @@ -10,6 +10,7 @@ use leptos_use::{ use_websocket_with_options, }; use reactive_stores::Store; +use werewolves_proto::message::host::ServerToHostMessage; use werewolves_proto::message::{ClientMessage, host::HostMessage}; use werewolves_proto::message::{IntoClientResponse, WrappedServerMessage}; @@ -91,7 +92,18 @@ pub fn GamePage(error: WriteSignal>) -> impl IntoView { } match message.clone() { Some(IntoClientResponse::Host(host_msg)) => { - log::debug!("got host message: {:?}", host_msg.title()); + match &host_msg { + ServerToHostMessage::ActionPrompt(prompt, page) => { + log::debug!( + "got host message: ActionPrompt({page}, {:?})", + prompt.title() + ) + } + ServerToHostMessage::ActionResult(_, result) => { + log::debug!("got host message: ActionResult({:?})", result.title()) + } + _ => log::debug!("got host message: {:?}", host_msg.title()), + }; host_message.set(Some(host_msg)); } Some(IntoClientResponse::Player(player_msg)) => { diff --git a/werewolves/src/app/pages/game/big.rs b/werewolves/src/app/pages/game/big.rs index c923abf..e777cf4 100644 --- a/werewolves/src/app/pages/game/big.rs +++ b/werewolves/src/app/pages/game/big.rs @@ -236,6 +236,7 @@ pub fn BigScreen() -> impl IntoView {
{content}
- }.into_any() + } + .into_any() } } diff --git a/werewolves/src/app/pages/game/host.rs b/werewolves/src/app/pages/game/host.rs index 7946e03..5af34a5 100644 --- a/werewolves/src/app/pages/game/host.rs +++ b/werewolves/src/app/pages/game/host.rs @@ -1,6 +1,9 @@ werewolves_macros::include_path!("werewolves/src/app/pages/game/host"); +mod overrides { + werewolves_macros::include_path!("werewolves/src/app/pages/game/host/overrides"); +} -use core::num::NonZeroU8; +use core::{num::NonZeroU8, ops::Not}; use std::collections::HashMap; use leptos::prelude::*; @@ -16,13 +19,17 @@ use werewolves_proto::{ use crate::app::{ Preferences, - components::DialogModal, - pages::night_actions::{RolePrompt, RoleResult}, + components::{DialogModal, HostInGameNav}, + pages::{ + game::host::overrides::Overrides, + host::overrides::OverrideScreen, + night_actions::{RolePrompt, RoleResult}, + }, }; use crate::{ConsoleLogError, app::error::WolfError}; #[derive(Debug, Clone, PartialEq, Default)] -enum HostPage { +pub enum HostPage { #[default] None, Settings, @@ -56,6 +63,13 @@ pub fn HostGamePage( let players: RwSignal> = RwSignal::new(Box::new([])); let dialog_open = RwSignal::new(HashMap::new()); let acks: RwSignal> = RwSignal::new(Box::new([])); + let night = RwSignal::new(false); + let show_back_button_only = RwSignal::new(false); + let on_overrides_screen = RwSignal::new(false); + let overrides_screen: RwSignal> = RwSignal::new(None); + let override_page_number = RwSignal::new(0usize); + let prompt_page = RwSignal::new(0usize); + let target_picker_count = RwSignal::new(16u8); let open_categories = RwSignal::new( Category::ALL @@ -115,6 +129,7 @@ pub fn HostGamePage( page.set(HostPage::RoleRevealAcks); } Srv2Host::ActionPrompt(prompt, prompt_page) => { + override_page_number.set(prompt_page); page.set(HostPage::ActionPrompt { prompt, page: prompt_page, @@ -146,7 +161,27 @@ pub fn HostGamePage( .is_some() .then_some(view! { }) }; - let content = move || match page.get() { + Effect::new(move || match page.get() { + HostPage::None + | HostPage::Settings + | HostPage::RoleRevealAcks + | HostPage::Daytime { .. } => night.set(false), + HostPage::ActionPrompt { .. } | HostPage::ActionResult { .. } => night.set(true), + }); + let content = move || { + if on_overrides_screen.get() { + show_back_button_only.set(true); + return view! { + + } + .into_any(); + } + match page.get() { HostPage::None => ().into_any(), HostPage::Settings => view! { view! { - + } => { + view! { } + .into_any() + } + } + }; + + move || match page.get() { + HostPage::Settings | HostPage::None => view! { + {cancel} + {content} } .into_any(), - }; - view! { - {cancel} - {content} + HostPage::RoleRevealAcks + | HostPage::ActionPrompt { .. } + | HostPage::ActionResult { .. } + | HostPage::Daytime { .. } => { + let cancel = matches!(&*page.read(), HostPage::Daytime { .. }) + .not() + .then_some(cancel); + view! { +
+ +
{cancel} {content}
+
+ } + .into_any() + } } } diff --git a/werewolves/src/app/pages/game/host/daytime.rs b/werewolves/src/app/pages/game/host/daytime.rs index 4fd08c3..49d0e45 100644 --- a/werewolves/src/app/pages/game/host/daytime.rs +++ b/werewolves/src/app/pages/game/host/daytime.rs @@ -65,7 +65,7 @@ pub fn DaytimePlayerList( .collect::>(); kills.is_empty().not().then_some(view! {
- + "died last night"
{kills.into_iter().collect_view()}
}) @@ -127,10 +127,7 @@ pub fn DaytimePlayerList(

{confirmation_text.clone()}

}; @@ -174,7 +171,12 @@ fn DaytimePlayer( let text = role.to_string().to_case(Case::Title); let align_class = role.wolf().then_some("red"); view! { - + {screen_opts} + + } +} + +#[component] +pub fn TestScreenSelector( + screen: RwSignal>, + send: WriteSignal>, +) -> impl IntoView { + let prompts = move || { + ActionPromptTitle::ALL + .into_iter() + .map(|title| { + let OverrideScreen::Prompt(prompt) = Into::::into(title) else { + unreachable!() + }; + let picked_class = if let Some(OverrideScreen::Prompt(current)) = screen.get() + && current.title() == title + { + Some("selected") + } else { + None + }; + let callback = move |_| { + let OverrideScreen::Prompt(prompt) = Into::::into(title) else { + unreachable!() + }; + screen.set(Some(OverrideScreen::Prompt(prompt.clone()))); + send.set(Some(ServerToHostMessage::ActionPrompt(prompt, 0))); + }; + let class = prompt_class(&prompt); + view! { +
  • + +
  • + } + }) + .collect_view() + }; + + let results = move || { + ActionResultTitle::ALL + .into_iter() + .filter(|title| !matches!(title, ActionResultTitle::Continue)) + .map(|title| { + let OverrideScreen::Result(result) = Into::::into(title) else { + unreachable!() + }; + let picked_class = if let Some(OverrideScreen::Result(current)) = screen.get() + && current.title() == title + { + Some("selected") + } else { + None + }; + let callback = move |_| { + let OverrideScreen::Result(result) = Into::::into(title) else { + unreachable!() + }; + screen.set(Some(OverrideScreen::Result(result.clone()))); + send.set(Some(ServerToHostMessage::ActionResult(None, result))); + }; + let class = result_class(&result); + view! { +
  • + +
  • + } + }) + .collect_view() + }; + view! { +
    +
    + "prompts" +
      {prompts}
    +
    +
    + "results" +
      {results}
    +
    +
    + } +} + +pub(super) fn identities(num: usize) -> Box<[CharacterIdentity]> { + (1..=num) + .map(|num| { + CharacterIdentity::new( + CharacterId::from_u128(num as _), + format!("Player {num}"), + Some("they/them".into()), + NonZeroU8::new(num as _).unwrap(), + ) + }) + .collect() +} + +pub(super) fn identity() -> CharacterIdentity { + identities(1).into_iter().next().unwrap() +} + +#[derive(Debug, Clone, PartialEq)] +pub enum OverrideScreen { + Prompt(ActionPrompt), + Result(ActionResult), + TargetPicker(Box<[CharacterIdentity]>), +} + +impl From for OverrideScreen { + fn from(value: ActionResultTitle) -> Self { + OverrideScreen::Result(match value { + ActionResultTitle::SkippedByHost => ActionResult::SkippedByHost, + ActionResultTitle::RoleBlocked => ActionResult::RoleBlocked, + ActionResultTitle::Drunk => ActionResult::Drunk, + ActionResultTitle::Seer => ActionResult::Seer(identity(), Alignment::Village), + ActionResultTitle::PowerSeer => ActionResult::PowerSeer { + target: identity(), + powerful: Powerful::Powerful, + }, + ActionResultTitle::Adjudicator => ActionResult::Adjudicator { + target: identity(), + killer: Killer::Killer, + }, + ActionResultTitle::Arcanist => ActionResult::Arcanist( + (identity(), identities(2).last().cloned().unwrap()), + AlignmentEq::Same, + ), + ActionResultTitle::GraveDigger => ActionResult::GraveDigger(identity(), None), + ActionResultTitle::Mortician => { + ActionResult::Mortician(identity(), DiedToTitle::Execution) + } + ActionResultTitle::Insomniac => ActionResult::Insomniac(Visits::new(identities(2))), + ActionResultTitle::Empath => ActionResult::Empath { + target: identity(), + scapegoat: true, + }, + ActionResultTitle::BeholderSawNothing => ActionResult::BeholderSawNothing, + ActionResultTitle::BeholderSawEverything => ActionResult::BeholderSawEverything, + ActionResultTitle::GoBackToSleep => ActionResult::GoBackToSleep, + ActionResultTitle::ShiftFailed => ActionResult::ShiftFailed, + ActionResultTitle::Continue => ActionResult::Continue, + }) + } +} + +impl From for OverrideScreen { + fn from(value: ActionPromptTitle) -> Self { + Self::Prompt(match value { + ActionPromptTitle::BeholderWakes => ActionPrompt::BeholderWakes { + character_id: identity(), + }, + ActionPromptTitle::CoverOfDarkness => ActionPrompt::CoverOfDarkness, + ActionPromptTitle::WolvesIntro => ActionPrompt::WolvesIntro { + wolves: identities(5) + .into_iter() + .zip([ + RoleTitle::Werewolf, + RoleTitle::AlphaWolf, + RoleTitle::DireWolf, + RoleTitle::LoneWolf, + RoleTitle::Bloodletter, + ]) + .collect(), + }, + ActionPromptTitle::RoleChange => ActionPrompt::RoleChange { + character_id: identities(1).into_iter().next().unwrap(), + new_role: RoleTitle::Adjudicator, + }, + ActionPromptTitle::ElderReveal => ActionPrompt::ElderReveal { + character_id: identities(1).into_iter().next().unwrap(), + }, + ActionPromptTitle::Seer => ActionPrompt::Seer { + character_id: identities(1).into_iter().next().unwrap(), + living_players: identities(20), + marked: None, + }, + ActionPromptTitle::Protector => ActionPrompt::Protector { + character_id: identities(1).into_iter().next().unwrap(), + targets: identities(20), + marked: None, + }, + ActionPromptTitle::Arcanist => ActionPrompt::Arcanist { + character_id: identities(1).into_iter().next().unwrap(), + living_players: identities(20), + marked: (None, None), + }, + ActionPromptTitle::Gravedigger => ActionPrompt::Gravedigger { + character_id: identities(1).into_iter().next().unwrap(), + dead_players: identities(20), + marked: None, + }, + ActionPromptTitle::Hunter => ActionPrompt::Hunter { + character_id: identities(1).into_iter().next().unwrap(), + current_target: None, + living_players: identities(20), + marked: None, + }, + ActionPromptTitle::Militia => ActionPrompt::Militia { + character_id: identities(1).into_iter().next().unwrap(), + living_players: identities(20), + marked: None, + }, + ActionPromptTitle::MapleWolf => ActionPrompt::MapleWolf { + character_id: identities(1).into_iter().next().unwrap(), + nights_til_starvation: 0, + living_players: identities(20), + marked: None, + }, + ActionPromptTitle::Guardian => ActionPrompt::Guardian { + character_id: identities(1).into_iter().next().unwrap(), + previous: None, + living_players: identities(20), + marked: None, + }, + ActionPromptTitle::Adjudicator => ActionPrompt::Adjudicator { + character_id: identities(1).into_iter().next().unwrap(), + living_players: identities(20), + marked: None, + }, + ActionPromptTitle::PowerSeer => ActionPrompt::PowerSeer { + character_id: identities(1).into_iter().next().unwrap(), + living_players: identities(20), + marked: None, + }, + ActionPromptTitle::Mortician => ActionPrompt::Mortician { + character_id: identities(1).into_iter().next().unwrap(), + dead_players: identities(20), + marked: None, + }, + ActionPromptTitle::BeholderChooses => ActionPrompt::BeholderChooses { + character_id: identities(1).into_iter().next().unwrap(), + living_players: identities(20), + marked: None, + }, + ActionPromptTitle::MasonsWake => ActionPrompt::MasonsWake { + leader: identities(1).into_iter().next().unwrap(), + masons: identities(3), + }, + ActionPromptTitle::MasonLeaderRecruit => ActionPrompt::MasonLeaderRecruit { + character_id: identities(1).into_iter().next().unwrap(), + recruits_left: NonZeroU8::new(3).unwrap(), + potential_recruits: identities(20), + marked: None, + }, + ActionPromptTitle::Empath => ActionPrompt::Empath { + character_id: identities(1).into_iter().next().unwrap(), + living_players: identities(20), + marked: None, + }, + ActionPromptTitle::Vindicator => ActionPrompt::Vindicator { + character_id: identities(1).into_iter().next().unwrap(), + living_players: identities(20), + marked: None, + }, + ActionPromptTitle::PyreMaster => ActionPrompt::PyreMaster { + character_id: identities(1).into_iter().next().unwrap(), + living_players: identities(20), + marked: None, + }, + ActionPromptTitle::WolfPackKill => ActionPrompt::WolfPackKill { + living_villagers: identities(20), + marked: None, + }, + ActionPromptTitle::Shapeshifter => ActionPrompt::Shapeshifter { + character_id: identities(1).into_iter().next().unwrap(), + }, + ActionPromptTitle::AlphaWolf => ActionPrompt::AlphaWolf { + character_id: identities(1).into_iter().next().unwrap(), + living_villagers: identities(20), + marked: None, + }, + ActionPromptTitle::DireWolf => ActionPrompt::DireWolf { + character_id: identities(1).into_iter().next().unwrap(), + living_players: identities(20), + marked: None, + }, + ActionPromptTitle::LoneWolfKill => ActionPrompt::LoneWolfKill { + character_id: identities(1).into_iter().next().unwrap(), + living_players: identities(20), + marked: None, + }, + ActionPromptTitle::Insomniac => ActionPrompt::Insomniac { + character_id: identities(1).into_iter().next().unwrap(), + }, + ActionPromptTitle::Bloodletter => ActionPrompt::Bloodletter { + character_id: identities(1).into_iter().next().unwrap(), + living_players: identities(20), + marked: None, + }, + ActionPromptTitle::DamnedIntro => ActionPrompt::DamnedIntro { + character_id: identities(1).into_iter().next().unwrap(), + }, + }) + } +} + +fn result_class(result: &ActionResult) -> Option<&'static str> { + match result.title() { + ActionResultTitle::Drunk | ActionResultTitle::RoleBlocked => Some("drunk"), + ActionResultTitle::PowerSeer + | ActionResultTitle::Adjudicator + | ActionResultTitle::Arcanist + | ActionResultTitle::GraveDigger + | ActionResultTitle::Mortician + | ActionResultTitle::Insomniac + | ActionResultTitle::Empath + | ActionResultTitle::BeholderSawNothing + | ActionResultTitle::BeholderSawEverything + | ActionResultTitle::Seer => Some("intel"), + ActionResultTitle::ShiftFailed => Some("wolves"), + ActionResultTitle::GoBackToSleep + | ActionResultTitle::Continue + | ActionResultTitle::SkippedByHost => None, + } +} + +fn prompt_class(prompt: &ActionPrompt) -> Option<&'static str> { + match prompt { + ActionPrompt::ElderReveal { .. } + | ActionPrompt::RoleChange { .. } + | ActionPrompt::CoverOfDarkness => None, + ActionPrompt::BeholderWakes { .. } + | ActionPrompt::Seer { .. } + | ActionPrompt::Arcanist { .. } + | ActionPrompt::Gravedigger { .. } + | ActionPrompt::Adjudicator { .. } + | ActionPrompt::PowerSeer { .. } + | ActionPrompt::Mortician { .. } + | ActionPrompt::BeholderChooses { .. } + | ActionPrompt::MasonsWake { .. } + | ActionPrompt::MasonLeaderRecruit { .. } + | ActionPrompt::Empath { .. } => Some("intel"), + ActionPrompt::Protector { .. } + | ActionPrompt::Guardian { .. } + | ActionPrompt::Vindicator { .. } => Some("defensive"), + ActionPrompt::Hunter { .. } + | ActionPrompt::Militia { .. } + | ActionPrompt::MapleWolf { .. } + | ActionPrompt::PyreMaster { .. } => Some("offensive"), + ActionPrompt::WolvesIntro { .. } + | ActionPrompt::WolfPackKill { .. } + | ActionPrompt::Shapeshifter { .. } + | ActionPrompt::AlphaWolf { .. } + | ActionPrompt::DireWolf { .. } + | ActionPrompt::LoneWolfKill { .. } + | ActionPrompt::Insomniac { .. } + | ActionPrompt::Bloodletter { .. } => Some("wolves"), + ActionPrompt::DamnedIntro { .. } => Some("damned"), + } +} diff --git a/werewolves/src/app/pages/game/host/overrides/prompt.rs b/werewolves/src/app/pages/game/host/overrides/prompt.rs new file mode 100644 index 0000000..a08edcd --- /dev/null +++ b/werewolves/src/app/pages/game/host/overrides/prompt.rs @@ -0,0 +1,378 @@ +use convert_case::{Case, Casing}; +use gloo::dialogs::prompt; +// 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::{ + ev::{Event, Targeted, WheelEvent}, + prelude::*, + web_sys::HtmlSelectElement, +}; + +use core::num::NonZeroU8; + +use werewolves_proto::{ + message::{host::ServerToHostMessage, night::ActionPrompt}, + role::{PreviousGuardianAction, RoleTitle}, +}; + +use crate::app::{ + components::IncDecU8, + pages::{game::host::overrides, night_actions}, +}; + +#[component] +pub fn PromptScreenTest( + prompt: RwSignal>, + page: RwSignal, + send: WriteSignal>, +) -> impl IntoView { + let options = move || { + let prompt_signal = prompt; + let Some(prompt) = prompt.get() else { + return ().into_any(); + }; + match prompt { + ActionPrompt::WolvesIntro { wolves } => { + let new_count = RwSignal::new(wolves.len() as u8); + + Effect::new(move || { + let new_prompt = ActionPrompt::WolvesIntro { + wolves: overrides::identities(new_count.get() as _) + .into_iter() + .zip(RoleTitle::ALL.into_iter().filter(|w| w.wolf()).cycle()) + .collect(), + }; + send.set(Some(ServerToHostMessage::ActionPrompt( + new_prompt.clone(), + 0, + ))); + }); + view! { + "wolf count" + + } + .into_any() + } + ActionPrompt::RoleChange { new_role, .. } => { + let roles = RoleTitle::ALL + .into_iter() + .map(|role| { + view! { } + }) + .collect_view(); + let on_change_cb = move |t: Targeted| { + let select = t.target(); + let selected = select.selected_index(); + if selected == -1 { + return; + } + if let Some(new_role) = RoleTitle::ALL.into_iter().nth(selected as _) { + let new_prompt = ActionPrompt::RoleChange { + character_id: super::identity(), + new_role, + }; + send.set(Some(ServerToHostMessage::ActionPrompt( + new_prompt.clone(), + 0, + ))); + } + }; + + let on_wheel = move |t: Targeted| { + let target = t.target(); + let index = target.selected_index(); + let new_index = match t.delta_y().total_cmp(&0.0) { + core::cmp::Ordering::Equal => return, + core::cmp::Ordering::Less => { + if index != 0 { + index - 1 + } else { + (target.children().length() - 1) as i32 + } + } + core::cmp::Ordering::Greater => { + if index + 1 < target.children().length() as i32 { + index + 1 + } else { + 0 + } + } + }; + target.set_selected_index(new_index); + if let Some(new_role) = RoleTitle::ALL.into_iter().nth(new_index as _) { + let new_prompt = ActionPrompt::RoleChange { + character_id: super::identity(), + new_role, + }; + send.set(Some(ServerToHostMessage::ActionPrompt( + new_prompt.clone(), + 0, + ))); + } + }; + view! { +
    + {"new role"} + +
    + } + .into_any() + } + + ActionPrompt::Hunter { current_target, .. } => { + let toggle_target = current_target.is_none().then_some(super::identity()); + let button_text = if toggle_target.is_some() { + "remove previous target" + } else { + "set previous target" + }; + let on_toggle = move |_| { + let new_prompt = ActionPrompt::Hunter { + character_id: super::identity(), + current_target: toggle_target.clone(), + living_players: overrides::identities(20), + marked: None, + }; + send.set(Some(ServerToHostMessage::ActionPrompt( + new_prompt.clone(), + 0, + ))); + }; + + view! { +
    + +
    + } + .into_any() + } + ActionPrompt::MapleWolf { + nights_til_starvation, + .. + } => { + let nights_til_starvation = RwSignal::new(nights_til_starvation); + Effect::new(move || { + let new_prompt = ActionPrompt::MapleWolf { + character_id: super::identity(), + nights_til_starvation: nights_til_starvation.get(), + living_players: overrides::identities(20), + marked: None, + }; + send.set(Some(ServerToHostMessage::ActionPrompt( + new_prompt.clone(), + 0, + ))); + }); + + view! { +
    + "nights til starvation" + +
    + } + .into_any() + } + ActionPrompt::Guardian { .. } => { + let none_disabled = move || { + matches!( + prompt_signal.get(), + Some(ActionPrompt::Guardian { previous: None, .. }) + ) + }; + let prev_none = move |_| { + let new_prompt = ActionPrompt::Guardian { + character_id: super::identity(), + previous: None, + living_players: super::identities(20), + marked: None, + }; + send.set(Some(ServerToHostMessage::ActionPrompt( + new_prompt.clone(), + 0, + ))); + }; + + let p = prompt.clone(); + let prot_disabled = move || { + matches!( + p, + ActionPrompt::Guardian { + previous: Some(PreviousGuardianAction::Protect(_)), + .. + } + ) + }; + let prev_prot = move |_| { + let new_prompt = ActionPrompt::Guardian { + character_id: super::identity(), + previous: Some(PreviousGuardianAction::Protect( + super::identities(2).into_iter().nth(1).unwrap(), + )), + living_players: super::identities(20), + marked: None, + }; + send.set(Some(ServerToHostMessage::ActionPrompt( + new_prompt.clone(), + 0, + ))); + }; + let p = prompt.clone(); + let guard_disabled = move || { + matches!( + p, + ActionPrompt::Guardian { + previous: Some(PreviousGuardianAction::Guard(_)), + .. + } + ) + }; + let prev_guard = move |_| { + let new_prompt = ActionPrompt::Guardian { + character_id: super::identity(), + previous: Some(PreviousGuardianAction::Guard( + super::identities(2).into_iter().nth(1).unwrap(), + )), + living_players: super::identities(20), + marked: None, + }; + send.set(Some(ServerToHostMessage::ActionPrompt( + new_prompt.clone(), + 0, + ))); + }; + + view! { + "previous protect" +
    + + + +
    + } + .into_any() + } + ActionPrompt::MasonsWake { masons, .. } => { + let mason_count = RwSignal::new(masons.len() as u8); + Effect::new(move || { + let new_prompt = ActionPrompt::MasonsWake { + leader: super::identity(), + masons: super::identities(mason_count.get() as _), + }; + send.set(Some(ServerToHostMessage::ActionPrompt( + new_prompt.clone(), + 0, + ))); + }); + view! { + "masons" + + } + .into_any() + } + ActionPrompt::MasonLeaderRecruit { recruits_left, .. } => { + let recruits_left = RwSignal::new(recruits_left.get()); + Effect::new(move || { + let new_prompt = ActionPrompt::MasonLeaderRecruit { + character_id: super::identity(), + recruits_left: NonZeroU8::new(recruits_left.get()).unwrap(), + potential_recruits: overrides::identities(20), + marked: None, + }; + send.set(Some(ServerToHostMessage::ActionPrompt( + new_prompt.clone(), + 0, + ))); + }); + view! { }.into_any() + } + + ActionPrompt::BeholderWakes { .. } + | ActionPrompt::Protector { .. } + | ActionPrompt::Arcanist { .. } + | ActionPrompt::Gravedigger { .. } + | ActionPrompt::Militia { .. } + | ActionPrompt::Adjudicator { .. } + | ActionPrompt::PowerSeer { .. } + | ActionPrompt::Mortician { .. } + | ActionPrompt::BeholderChooses { .. } + | ActionPrompt::Empath { .. } + | ActionPrompt::Vindicator { .. } + | ActionPrompt::PyreMaster { .. } + | ActionPrompt::WolfPackKill { .. } + | ActionPrompt::AlphaWolf { .. } + | ActionPrompt::DireWolf { .. } + | ActionPrompt::LoneWolfKill { .. } + | ActionPrompt::Bloodletter { .. } + | ActionPrompt::Seer { .. } + | ActionPrompt::ElderReveal { .. } + | ActionPrompt::DamnedIntro { .. } + | ActionPrompt::Insomniac { .. } + | ActionPrompt::Shapeshifter { .. } + | ActionPrompt::CoverOfDarkness => ().into_any(), + } + }; + let prev_page_disabled = move || page.get() == 0; + let prev_page = move |_| { + if let Some(prompt) = prompt.get() { + let new_page = page.get().saturating_sub(1); + page.set(new_page); + send.set(Some(ServerToHostMessage::ActionPrompt(prompt, new_page))); + } + }; + + let next_page = move |_| { + if let Some(prompt) = prompt.get() { + let new_page = page.get().saturating_add(1); + page.set(new_page); + send.set(Some(ServerToHostMessage::ActionPrompt(prompt, new_page))); + } + }; + let title = move || { + prompt + .get() + .map(|p| p.title().to_string()) + .unwrap_or_default() + }; + let is_at_max_page = move || { + let Some(prompt) = prompt.get() else { + return true; + }; + night_actions::pages_for_prompt(prompt, None).len() <= page.get().saturating_add(1) + }; + view! { +
    + {title} +
    + "page:" + + {move || page.get().saturating_add(1)} + +
    +
    {options}
    +
    + } +} diff --git a/werewolves/src/app/pages/game/host/overrides/result.rs b/werewolves/src/app/pages/game/host/overrides/result.rs new file mode 100644 index 0000000..d05336e --- /dev/null +++ b/werewolves/src/app/pages/game/host/overrides/result.rs @@ -0,0 +1,312 @@ +// 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::{ + ev::{Event, Targeted, WheelEvent}, + prelude::*, + web_sys::HtmlSelectElement, +}; + +use std::rc::Rc; + +use werewolves_proto::{ + diedto::DiedToTitle, + message::{ + host::ServerToHostMessage, + night::{ActionResult, Visits}, + }, + role::{Alignment, RoleTitle}, +}; + +use crate::app::{components::IncDecU8, pages::game::host::overrides}; + +#[component] +pub fn ResultScreenTest( + result: ActionResult, + send: WriteSignal>, +) -> impl IntoView { + let options = match result.clone() { + ActionResult::BeholderSawNothing + | ActionResult::BeholderSawEverything + | ActionResult::GoBackToSleep + | ActionResult::ShiftFailed + | ActionResult::Continue + | ActionResult::Drunk + | ActionResult::RoleBlocked + | ActionResult::SkippedByHost => ().into_any(), + ActionResult::Seer(target, alignment) => { + let all = Alignment::ALL + .into_iter() + .map(|align| { + let target = target.clone(); + let on_click = move |_| { + send.set(Some(ServerToHostMessage::ActionResult( + None, + ActionResult::Seer(target.clone(), align), + ))); + }; + view! { + + } + }) + .collect_view(); + view! { +
    +
    {all}
    +
    + } + .into_any() + } + ActionResult::PowerSeer { target, powerful } => { + let on_toggle = move |_| { + send.set(Some(ServerToHostMessage::ActionResult( + None, + ActionResult::PowerSeer { + target: target.clone(), + powerful: !powerful, + }, + ))); + }; + let text = if powerful.powerful() { + "make not powerful" + } else { + "make powerful" + }; + view! { +
    + +
    + } + .into_any() + } + ActionResult::Adjudicator { target, killer } => { + let on_toggle = move |_| { + send.set(Some(ServerToHostMessage::ActionResult( + None, + ActionResult::Adjudicator { + target: target.clone(), + killer: !killer, + }, + ))); + }; + let text = if killer.killer() { + "make not killer" + } else { + "make killer" + }; + view! { +
    + +
    + } + .into_any() + } + ActionResult::Arcanist(targets, alignment_eq) => { + let on_toggle = move |_| { + send.set(Some(ServerToHostMessage::ActionResult( + None, + ActionResult::Arcanist(targets.clone(), !alignment_eq), + ))); + }; + let text = if alignment_eq.same() { + "make different" + } else { + "make same" + }; + view! { +
    + +
    + } + .into_any() + } + ActionResult::GraveDigger(target, role_title) => { + let possibilities = [None] + .into_iter() + .chain(RoleTitle::ALL.into_iter().map(Some)) + .collect::>(); + let roles = possibilities + .iter() + .map(|role| { + let text = role + .as_ref() + .map(|r| r.to_string()) + .unwrap_or(String::from("empty grave")); + view! { } + }) + .collect_view(); + + let res_target = target.clone(); + let p = possibilities.clone(); + let on_change_cb = move |ev: Targeted| { + let select = ev.target(); + let selected = select.selected_index(); + if selected == -1 { + return; + } + if let Some(new_role) = p.get(selected as usize) { + send.set(Some(ServerToHostMessage::ActionResult( + None, + ActionResult::GraveDigger(res_target.clone(), *new_role), + ))); + } + }; + let res_target = target.clone(); + let on_wheel = move |ev: Targeted| { + let target = ev.target(); + let index = target.selected_index(); + let new_index = match ev.delta_y().total_cmp(&0.0) { + core::cmp::Ordering::Equal => return, + core::cmp::Ordering::Less => { + if index != 0 { + index - 1 + } else { + (target.children().length() - 1) as i32 + } + } + core::cmp::Ordering::Greater => { + if index + 1 < target.children().length() as i32 { + index + 1 + } else { + 0 + } + } + }; + target.set_selected_index(new_index); + if let Some(new_role) = possibilities.get(new_index as usize) { + send.set(Some(ServerToHostMessage::ActionResult( + None, + ActionResult::GraveDigger(res_target.clone(), *new_role), + ))); + } + }; + view! { +
    + + +
    + } + .into_any() + } + ActionResult::Mortician(target, died_to_title) => { + let roles = DiedToTitle::ALL + .into_iter() + .map(|died_to| { + view! { } + }) + .collect_view(); + let res_target = target.clone(); + let on_change_cb = move |ev: Targeted| { + let select = ev.target(); + let selected = select.selected_index(); + if selected == -1 { + return; + } + if let Some(died_to) = DiedToTitle::ALL.into_iter().nth(selected as _) { + send.set(Some(ServerToHostMessage::ActionResult( + None, + ActionResult::Mortician(res_target.clone(), died_to), + ))); + } + }; + let res_target = target.clone(); + let on_wheel = move |ev: Targeted| { + let target = ev.target(); + let index = target.selected_index(); + let new_index = match ev.delta_y().total_cmp(&0.0) { + core::cmp::Ordering::Equal => return, + core::cmp::Ordering::Less => { + if index != 0 { + index - 1 + } else { + (target.children().length() - 1) as i32 + } + } + core::cmp::Ordering::Greater => { + if index + 1 < target.children().length() as i32 { + index + 1 + } else { + 0 + } + } + }; + target.set_selected_index(new_index); + if let Some(died_to) = DiedToTitle::ALL.into_iter().nth(new_index as _) { + send.set(Some(ServerToHostMessage::ActionResult( + None, + ActionResult::Mortician(res_target.clone(), died_to), + ))); + } + }; + view! { +
    + + +
    + } + .into_any() + } + ActionResult::Insomniac(visits) => { + let visits = RwSignal::new(visits.len() as u8); + Effect::new(move || { + send.set(Some(ServerToHostMessage::ActionResult( + None, + ActionResult::Insomniac(Visits::new(overrides::identities(visits.get() as _))), + ))) + }); + view! { +
    + "visits" + +
    + } + .into_any() + } + ActionResult::Empath { target, scapegoat } => { + let on_toggle = move |_| { + send.set(Some(ServerToHostMessage::ActionResult( + None, + ActionResult::Empath { + scapegoat: !scapegoat, + target: target.clone(), + }, + ))); + }; + let text = if scapegoat { + "make not scapegoat" + } else { + "make scapegoat" + }; + view! { +
    + +
    + } + .into_any() + } + }; + view! { +
    + {result.title().to_string()} +
    {options}
    +
    + } + .into_any() +} diff --git a/werewolves/src/app/pages/lobby.rs b/werewolves/src/app/pages/lobby.rs deleted file mode 100644 index bdf4af8..0000000 --- a/werewolves/src/app/pages/lobby.rs +++ /dev/null @@ -1,4 +0,0 @@ -use leptos::prelude::*; - -#[component] -pub fn HostLobby() -> impl IntoView {} diff --git a/werewolves/src/app/pages/night_actions/night_actions.rs b/werewolves/src/app/pages/night_actions/night_actions.rs index 8ec1cb0..51e17c7 100644 --- a/werewolves/src/app/pages/night_actions/night_actions.rs +++ b/werewolves/src/app/pages/night_actions/night_actions.rs @@ -1,3 +1,4 @@ +use convert_case::{Case, Casing}; use leptos::prelude::*; use werewolves_proto::{ character::CharacterId, @@ -6,12 +7,12 @@ use werewolves_proto::{ host::HostNightMessage, night::{ActionPrompt, ActionResponse}, }, - role::PreviousGuardianAction, + role::{PreviousGuardianAction, RoleTitle}, }; use crate::app::{ class::AsClasses, - components::{Cover, IdentityInline}, + components::{Cover, IdentityInline, Sample, TutorialBox}, error::WolfError, pages::night_actions::{ DamnedIntroPage1, DamnedIntroPage2, RoleChange, WolfPackKill, WolvesIntro, @@ -22,7 +23,7 @@ use crate::app::{ GuardianPagePreviousProtect2, HunterPage1, InsomniacPage1, LoneWolfPage1, MapleWolfPage1, MasonRecruitPage1, MasonRecruitPage2, MasonsWake, MilitiaPage1, MorticianPage1, PowerSeerPage1, ProtectorPage1, PyremasterPage1, SeerPage1, - VindicatorPage1, + ShapeshifterPage1, VindicatorPage1, }, }, }; @@ -31,27 +32,23 @@ pub trait RolePage { fn role_pages(&self, big_screen: bool) -> ViewFn; } -#[component] -pub fn RolePrompt( +pub fn target_picker_tutorials(prompt: &ActionPrompt) -> Vec { + #[allow(clippy::match_single_binding)] + match prompt { + _ => vec![], + } +} + +pub fn pages_for_prompt( prompt: ActionPrompt, - page: usize, - #[prop(optional)] reply: Option>>, - #[prop(optional)] error: Option>>, -) -> impl IntoView { + reply: Option>>, +) -> Vec { 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 { + match prompt { ActionPrompt::CoverOfDarkness => vec![match reply { Some(reply) => view! { }.into_any(), None => view! { }.into_any(), @@ -180,8 +177,8 @@ pub fn RolePrompt( ActionPrompt::PyreMaster { character_id, .. } => { vec![view! { {ident(character_id)} }.into_any()] } - ActionPrompt::Shapeshifter { .. } => { - vec![] + ActionPrompt::Shapeshifter { character_id } => { + vec![view! {{ident(character_id)} }.into_any()] } ActionPrompt::AlphaWolf { character_id, .. } => { vec![view! { {ident(character_id)} }.into_any()] @@ -224,7 +221,27 @@ pub fn RolePrompt( ActionPrompt::WolvesIntro { wolves } => { vec![view! { }.into_any()] } - }; + } +} + +#[component] +pub fn RolePrompt( + prompt: ActionPrompt, + page: usize, + #[prop(optional)] reply: Option>>, + #[prop(optional)] error: Option>>, +) -> impl IntoView { + 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 tutorials = target_picker_tutorials(&prompt); + let shapeshifter = matches!(prompt, ActionPrompt::Shapeshifter { .. }); + let mut pages: Vec = pages_for_prompt(prompt, reply); // 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); @@ -249,6 +266,23 @@ pub fn RolePrompt( }) { return page.into_any(); } + // shapeshifter gets a yes/no + if shapeshifter { + let decision = RwSignal::new(None); + if let Some(reply) = reply { + Effect::new(move || match decision.get() { + Some(true) => reply.set(Some(HostNightMessage::ActionResponse( + ActionResponse::Shapeshift, + ))), + Some(false) => reply.set(Some(HostNightMessage::ActionResponse( + ActionResponse::Continue, + ))), + None => {} + }); + } + + return view! { }.into_any(); + } let target_picker = match targets { Some(targets) => { let pick = RwSignal::new(None); @@ -288,8 +322,11 @@ pub fn RolePrompt( } }); + let tutorials = (continue_btn.is_some() && !tutorials.is_empty()) + .then(|| tutorials.into_iter().collect_view()); view! { {target_picker} + {tutorials} {continue_btn} } .into_any() @@ -323,5 +360,19 @@ pub fn TargetPicker( }) .collect_view(); - view! {
    {targets}
    } + view! {
    {targets}
    } +} + +#[component] +pub fn BooleanPicker(decision: WriteSignal>) -> impl IntoView { + view! { +
    + + +
    + } } diff --git a/werewolves/src/app/pages/night_actions/result.rs b/werewolves/src/app/pages/night_actions/result.rs index 0130688..9ababcc 100644 --- a/werewolves/src/app/pages/night_actions/result.rs +++ b/werewolves/src/app/pages/night_actions/result.rs @@ -8,10 +8,11 @@ use werewolves_proto::message::{ use crate::app::{ components::Cover, pages::night_actions::{ - DrunkPage, RoleblockPage, + DrunkPage, RoleblockPage, ShiftFailed, role::{ - AdjudicatorResult, ArcanistResult, EmpathResult, GravediggerResultPage, - InsomniacResult, MorticianResultPage, PowerSeerResult, SeerResult, + AdjudicatorResult, ArcanistResult, BeholderSawEverything, BeholderSawNothing, + EmpathResult, GravediggerResultPage, InsomniacResult, MorticianResultPage, + PowerSeerResult, SeerResult, }, }, }; @@ -23,57 +24,41 @@ 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::BeholderSawNothing => todo!(), - ActionResult::BeholderSawEverything => todo!(), + 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 => view! { }.into_any(), + ActionResult::BeholderSawEverything => view! { }.into_any(), ActionResult::GoBackToSleep => return match reply { Some(reply) => view! { } .into_any(), None => view! { }.into_any(), }, - ActionResult::ShiftFailed => todo!(), + ActionResult::ShiftFailed => view!{}.into_any(), + ActionResult::SkippedByHost | ActionResult::Continue => { Effect::new(move || { let Some(reply) = reply else { return; }; - reply.set(Some(HostNightMessage::Next)); + // NOTE: in the old code this was a GetState and the return was + // a blank + reply.set(Some(HostNightMessage::Next)); }); ().into_any() } - ActionResult::SkippedByHost => todo!(), }; let next_btn = reply.map(|reply| { view! { diff --git a/werewolves/src/app/pages/night_actions/role/adjudicator.rs b/werewolves/src/app/pages/night_actions/role/adjudicator.rs index 1d03b46..d2b38dc 100644 --- a/werewolves/src/app/pages/night_actions/role/adjudicator.rs +++ b/werewolves/src/app/pages/night_actions/role/adjudicator.rs @@ -23,9 +23,9 @@ pub fn AdjudicatorPage1() -> impl IntoView {
    "ADJUDICATOR"
    -

    "PICK A PLAYER"

    + "PICK A PLAYER" -

    "YOU WILL CHECK IF THEY APPEAR AS A KILLER"

    + "YOU WILL CHECK IF THEY APPEAR AS A KILLER"
    } @@ -47,7 +47,7 @@ pub fn AdjudicatorResult(killer: Killer, target: PublicIdentity) -> impl IntoVie
    {icon} -

    {text}

    + {text}
    } diff --git a/werewolves/src/app/pages/night_actions/role/arcanist.rs b/werewolves/src/app/pages/night_actions/role/arcanist.rs index 0230527..7d1f30e 100644 --- a/werewolves/src/app/pages/night_actions/role/arcanist.rs +++ b/werewolves/src/app/pages/night_actions/role/arcanist.rs @@ -39,12 +39,14 @@ pub fn ArcanistResult( targets: (PublicIdentity, PublicIdentity), ) -> impl IntoView { let text = match value { - AlignmentEq::Same => "ARE THE SAME", - AlignmentEq::Different => "ARE DIFFERENT", + AlignmentEq::Same => view! {"ARE THE SAME"}.into_any(), + AlignmentEq::Different => { + view! {"ARE ""DIFFERENT"}.into_any() + } }; let icons = match value { - AlignmentEq::Same => view! { }, - AlignmentEq::Different => view! { }, + AlignmentEq::Same => IconSource::BalancedScales, + AlignmentEq::Different => IconSource::UnbalancedScales, }; view! {
    @@ -55,7 +57,7 @@ pub fn ArcanistResult( "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 index 3529dd1..b99d7b0 100644 --- a/werewolves/src/app/pages/night_actions/role/beholder.rs +++ b/werewolves/src/app/pages/night_actions/role/beholder.rs @@ -49,9 +49,9 @@ pub fn BeholderSawNothing() -> impl IntoView {
    "BEHOLDER"
    -

    "YOUR TARGET HAS DIED"

    + "YOUR TARGET HAS DIED" -

    "BUT SAW NOTHING"

    + "BUT SAW NOTHING"
    } diff --git a/werewolves/src/app/pages/night_actions/role/bloodletter.rs b/werewolves/src/app/pages/night_actions/role/bloodletter.rs index 34697f2..447ae3b 100644 --- a/werewolves/src/app/pages/night_actions/role/bloodletter.rs +++ b/werewolves/src/app/pages/night_actions/role/bloodletter.rs @@ -14,7 +14,7 @@ // along with this program. If not, see . use leptos::prelude::*; -use crate::app::components::{Icon, IconSource}; +use crate::app::components::{Icon, IconSource, IconType}; #[component] pub fn BloodletterPage1() -> impl IntoView { @@ -24,9 +24,13 @@ pub fn BloodletterPage1() -> impl IntoView {
    "PICK A PLAYER" - "THEY'LL APPEAR AS A WOLF " "KILLER" - "AND POWERFUL" - "IN CHECKS FOR 2 NIGHTS" + "THEY'LL APPEAR AS A" "WOLF" + + "KILLER" + "AND" + "POWERFUL" + + "IN CHECKS FOR 2 NIGHTS"
    diff --git a/werewolves/src/app/pages/night_actions/role/shapeshifter.rs b/werewolves/src/app/pages/night_actions/role/shapeshifter.rs new file mode 100644 index 0000000..3a09453 --- /dev/null +++ b/werewolves/src/app/pages/night_actions/role/shapeshifter.rs @@ -0,0 +1,35 @@ +use leptos::prelude::*; + +use crate::app::components::{Icon, IconSource}; + +#[component] +pub fn ShapeshifterPage1() -> impl IntoView { + view! { +
    + "SHAPESHIFTER" +
    + + "WOULD YOU LIKE TO USE YOUR " {"ONCE PER GAME"} + " SHAPESHIFT ABILITY?" + + + "YOU WILL DIE" + ", BUT THE TARGET OF THE WOLFPACK KILL SHALL INSTEAD BECOME A WOLF" + +
    +
    + } +} + +#[component] +pub fn ShiftFailed() -> impl IntoView { + view! { +
    + "SHIFT FAILED" +
    + "YOUR SHIFT HAS FAILED" + "YOU RETAIN YOUR SHAPESHIFT ABILITY" +
    +
    + } +}