From 9b19b515813a77e55032224a5da99fac7cdc5747 Mon Sep 17 00:00:00 2001 From: emilis Date: Tue, 18 Nov 2025 14:35:35 +0000 Subject: [PATCH] maple wolf: show nights til starving --- werewolves-proto/src/character.rs | 3 +- werewolves-proto/src/game/night/next.rs | 4 +- werewolves-proto/src/game/night/process.rs | 4 +- werewolves-proto/src/message/night.rs | 2 +- werewolves/index.scss | 11 ++ werewolves/src/clients/mod.rs | 2 - werewolves/src/clients/test_remote.rs | 70 -------- werewolves/src/components/action/prompt.rs | 4 +- werewolves/src/components/inc_dec.rs | 96 ++++++++++ werewolves/src/main.rs | 14 +- werewolves/src/pages/role_page.rs | 10 +- werewolves/src/pages/role_page/maple_wolf.rs | 44 +++-- werewolves/src/test_util/mod.rs | 8 +- werewolves/src/test_util/prompt.rs | 174 ++++++------------- 14 files changed, 210 insertions(+), 236 deletions(-) delete mode 100644 werewolves/src/clients/test_remote.rs create mode 100644 werewolves/src/components/inc_dec.rs diff --git a/werewolves-proto/src/character.rs b/werewolves-proto/src/character.rs index 0cce86d..2b10931 100644 --- a/werewolves-proto/src/character.rs +++ b/werewolves-proto/src/character.rs @@ -482,7 +482,8 @@ impl Character { }), Role::MapleWolf { last_kill_on_night } => prompts.push(ActionPrompt::MapleWolf { character_id: self.identity(), - kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night, + nights_til_starvation: (last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get()) + - night, living_players: village.living_players_excluding(self.character_id()), marked: None, }), diff --git a/werewolves-proto/src/game/night/next.rs b/werewolves-proto/src/game/night/next.rs index 2895464..76d6a03 100644 --- a/werewolves-proto/src/game/night/next.rs +++ b/werewolves-proto/src/game/night/next.rs @@ -108,13 +108,13 @@ impl Night { Some(( ActionPrompt::MapleWolf { character_id, - kill_or_die, + nights_til_starvation, marked, .. }, _, )) => { - if *kill_or_die { + if *nights_til_starvation == 0 { (character_id.character_id, *marked) } else { return Ok(()); diff --git a/werewolves-proto/src/game/night/process.rs b/werewolves-proto/src/game/night/process.rs index 512e761..652f10d 100644 --- a/werewolves-proto/src/game/night/process.rs +++ b/werewolves-proto/src/game/night/process.rs @@ -238,8 +238,8 @@ impl Night { } ActionPrompt::MapleWolf { character_id, - kill_or_die, marked: Some(marked), + nights_til_starvation, .. } => Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::GoBackToSleep, @@ -248,7 +248,7 @@ impl Night { died_to: DiedTo::MapleWolf { night, source: character_id.character_id, - starves_if_fails: *kill_or_die, + starves_if_fails: *nights_til_starvation == 0, }, }), })), diff --git a/werewolves-proto/src/message/night.rs b/werewolves-proto/src/message/night.rs index f5ecb10..db0e9ed 100644 --- a/werewolves-proto/src/message/night.rs +++ b/werewolves-proto/src/message/night.rs @@ -111,7 +111,7 @@ pub enum ActionPrompt { #[checks(ActionType::Other)] MapleWolf { character_id: CharacterIdentity, - kill_or_die: bool, + nights_til_starvation: u8, living_players: Box<[CharacterIdentity]>, marked: Option, }, diff --git a/werewolves/index.scss b/werewolves/index.scss index 3c0b5b8..79bf15b 100644 --- a/werewolves/index.scss +++ b/werewolves/index.scss @@ -2412,3 +2412,14 @@ li.choice { } } } + +.inc-dec { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 20px; + + .inc-dec-content { + flex-grow: 1; + } +} diff --git a/werewolves/src/clients/mod.rs b/werewolves/src/clients/mod.rs index 4a5223c..0c66738 100644 --- a/werewolves/src/clients/mod.rs +++ b/werewolves/src/clients/mod.rs @@ -21,8 +21,6 @@ pub mod host { mod host; pub use host::*; } -pub mod test_remote; -// mod socket; const DEBUG_URL: &str = "ws://192.168.1.162:8080/connect/"; const LIVE_URL: &str = "wss://wolf.emilis.dev/connect/"; diff --git a/werewolves/src/clients/test_remote.rs b/werewolves/src/clients/test_remote.rs deleted file mode 100644 index 656d0d2..0000000 --- a/werewolves/src/clients/test_remote.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (C) 2025 Emilis Bliūdžius -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -use futures::{SinkExt, StreamExt, TryStreamExt, lock::Mutex}; -use gloo::net::websocket::{Message, futures::WebSocket}; -use werewolves_proto::message::host::{HostLobbyMessage, HostMessage, ServerToHostMessage}; -use yew::prelude::*; - -use crate::test_util::TestScreens; - -fn url() -> String { - format!( - "{}host", - option_env!("LOCAL") - .map(|_| crate::clients::DEBUG_URL) - .unwrap_or(crate::clients::LIVE_URL) - ) -} - -#[function_component] -pub fn TestClientRemote() -> Html { - gloo::utils::document().set_title("werewolves — remote test"); - let send = use_memo((), |_| Mutex::new(WebSocket::open(url().as_str()).unwrap())); - let send_cb = { - let send = send.clone(); - Callback::from(move |msg: ServerToHostMessage| { - let send = send.clone(); - yew::platform::spawn_local(async move { - send.lock() - .await - .send(gloo::net::websocket::Message::Bytes({ - let mut v = Vec::new(); - ciborium::into_writer(&HostMessage::Echo(msg.clone()), &mut v).unwrap(); - v - })) - .await - .unwrap(); - loop { - match send.lock().await.try_next().await { - Ok(Some(Message::Bytes(b))) => { - let recv: ServerToHostMessage = - ciborium::from_reader(b.as_slice()).unwrap(); - if recv == msg { - log::debug!("recv'd echo"); - return; - } - } - Ok(_) => panic!("got text message"), - Err(err) => panic!("{err}"), - } - } - }); - }) - }; - html! { - - } -} diff --git a/werewolves/src/components/action/prompt.rs b/werewolves/src/components/action/prompt.rs index 66c102e..b533fa4 100644 --- a/werewolves/src/components/action/prompt.rs +++ b/werewolves/src/components/action/prompt.rs @@ -375,14 +375,14 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { ), ActionPrompt::MapleWolf { character_id, - kill_or_die, + nights_til_starvation, living_players, marked, } => ( Some(character_id), living_players, marked.iter().cloned().collect(), - html! {<>{"maple wolf"} {kill_or_die.then_some(" — starving")}}, + html! {<>{"maple wolf"} {(*nights_til_starvation == 0).then_some(" — starving")}}, ), ActionPrompt::WolfPackKill { living_villagers, diff --git a/werewolves/src/components/inc_dec.rs b/werewolves/src/components/inc_dec.rs new file mode 100644 index 0000000..4eb5ca5 --- /dev/null +++ b/werewolves/src/components/inc_dec.rs @@ -0,0 +1,96 @@ +use core::ops::Range; + +// Copyright (C) 2025 Emilis Bliūdžius +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +use yew::prelude::*; + +use crate::components::Button; + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct IncDecU8Props { + #[prop_or_default] + pub children: Html, + #[prop_or_default] + pub class: yew::Classes, + #[prop_or_default] + pub button_class: yew::Classes, + pub value: u8, + pub value_range: Range, + pub on_update: Callback, +} + +#[function_component] +pub fn IncDecU8( + IncDecU8Props { + children, + class, + button_class, + value, + value_range, + on_update, + }: &IncDecU8Props, +) -> Html { + let value_state = use_state(|| *value); + let dec_disabled = value_range + .clone() + .next() + .and_then(|start| (start >= *value).then_some(String::from("already at minimum"))); + let dec = { + let current = *value; + let on_update = on_update.clone(); + let value_state = value_state.setter(); + Callback::from(move |_| { + let new = current.saturating_sub(1); + on_update.emit(new); + value_state.set(new); + }) + }; + let inc_disabled = value_range + .clone() + .last() + .and_then(|end| (*value >= end).then_some(String::from("already at max value"))); + let inc = { + let current = *value; + let on_update = on_update.clone(); + let value_state = value_state.setter(); + Callback::from(move |_| { + let new = current.saturating_add(1); + on_update.emit(new); + value_state.set(new); + }) + }; + html! { +
+ +
+ {*value_state} + {children.clone()} +
+ +
+ } +} diff --git a/werewolves/src/main.rs b/werewolves/src/main.rs index ceda047..64e3705 100644 --- a/werewolves/src/main.rs +++ b/werewolves/src/main.rs @@ -46,13 +46,9 @@ const BUILD_ID_LONG: &str = werewolves_macros::build_id_long!(); const BUILD_DIRTY: bool = werewolves_macros::build_dirty!(); const BUILD_TIME: &str = werewolves_macros::build_time!(); -use crate::{ - clients::{ - client::{Client2, ClientContext}, - host::{Host, HostEvent}, - test_remote::TestClientRemote, - }, - test_util::TestScreens, +use crate::clients::{ + client::{Client2, ClientContext}, + host::{Host, HostEvent}, }; fn main() { @@ -70,9 +66,7 @@ fn main() { let error_callback = Callback::from(move |err: Option| cb_clone.send_message(err)); - if path.starts_with("/host/test") { - yew::Renderer::::with_root(app_element).render(); - } else if path.starts_with("/host") { + if path.starts_with("/host") { let host = yew::Renderer::::with_root(app_element).render(); if path.starts_with("/host/big") { host.send_message(HostEvent::SetBigScreenState(true)); diff --git a/werewolves/src/pages/role_page.rs b/werewolves/src/pages/role_page.rs index 7711fa6..0de0bdd 100644 --- a/werewolves/src/pages/role_page.rs +++ b/werewolves/src/pages/role_page.rs @@ -109,14 +109,16 @@ impl RolePage for ActionPrompt { }]), ActionPrompt::MapleWolf { character_id, - kill_or_die, + nights_til_starvation, .. - } => Rc::new([html! { + } => [html! { <> {ident(character_id)} - + - }]), + }] + .into_iter() + .collect(), ActionPrompt::MasonLeaderRecruit { character_id, recruits_left, diff --git a/werewolves/src/pages/role_page/maple_wolf.rs b/werewolves/src/pages/role_page/maple_wolf.rs index 77e2ac6..8d23264 100644 --- a/werewolves/src/pages/role_page/maple_wolf.rs +++ b/werewolves/src/pages/role_page/maple_wolf.rs @@ -17,27 +17,45 @@ use yew::prelude::*; use crate::components::{Icon, IconSource, IconType}; #[derive(Debug, Clone, Copy, PartialEq, Properties)] -pub struct MapleWolfPage1Props { - pub starving: bool, +pub struct MapleWolfPageProps { + pub nights_til_starvation: u8, } #[function_component] -pub fn MapleWolfPage1(MapleWolfPage1Props { starving }: &MapleWolfPage1Props) -> Html { - let starving = starving - .then_some(html! { +pub fn MapleWolfPage1( + MapleWolfPageProps { + nights_til_starvation, + }: &MapleWolfPageProps, +) -> Html { + let food_state = if *nights_til_starvation == 0 { + html! {

{"YOU ARE STARVING"}

{"IF YOU FAIL TO EAT TONIGHT, YOU WILL DIE"}

- }) - .unwrap_or_else(|| { + } + } else { + let nights = if *nights_til_starvation == 1 { html! { -

- {"IF YOU DON'T EAT FOR TOO LONG, YOU WILL "} - {"STARVE"} -

+ <> + {"TOMORROW NIGHT "} + } - }); + } else { + html! { + <> + {"IN "}{*nights_til_starvation}{" NIGHTS "} + + } + }; + html! { +

+ {"IF YOU FAIL TO EAT "} + {nights} + {"YOU WILL "}{"STARVE"} +

+ } + }; html! {

{"MAPLE WOLF"}

@@ -48,7 +66,7 @@ pub fn MapleWolfPage1(MapleWolfPage1Props { starving }: &MapleWolfPage1Props) ->
- {starving} + {food_state}
} diff --git a/werewolves/src/test_util/mod.rs b/werewolves/src/test_util/mod.rs index e67d664..b94b3e1 100644 --- a/werewolves/src/test_util/mod.rs +++ b/werewolves/src/test_util/mod.rs @@ -21,10 +21,8 @@ use werewolves_proto::{ diedto::DiedToTitle, message::{ CharacterIdentity, - host::{HostMessage, ServerToHostMessage}, - night::{ - ActionPrompt, ActionPromptTitle, ActionResult, ActionResultTitle, ActionType, Visits, - }, + host::ServerToHostMessage, + night::{ActionPrompt, ActionPromptTitle, ActionResult, ActionResultTitle, Visits}, }, role::{Alignment, AlignmentEq, Killer, Powerful, RoleTitle}, }; @@ -304,7 +302,7 @@ impl From for TestScreen { }, ActionPromptTitle::MapleWolf => ActionPrompt::MapleWolf { character_id: identities(1).into_iter().next().unwrap(), - kill_or_die: false, + nights_til_starvation: 0, living_players: identities(20), marked: None, }, diff --git a/werewolves/src/test_util/prompt.rs b/werewolves/src/test_util/prompt.rs index d4c6782..689585b 100644 --- a/werewolves/src/test_util/prompt.rs +++ b/werewolves/src/test_util/prompt.rs @@ -18,15 +18,15 @@ use core::num::NonZeroU8; use web_sys::HtmlSelectElement; use werewolves_proto::{ - message::{ - host::{HostGameMessage, HostMessage, HostNightMessage, ServerToHostMessage}, - night::ActionPrompt, - }, - player::RoleChange, + message::{host::ServerToHostMessage, night::ActionPrompt}, role::{PreviousGuardianAction, RoleTitle}, }; -use crate::{components::Button, pages::RolePage, test_util::identities}; +use crate::{ + components::{Button, IncDecU8}, + pages::RolePage, + test_util::identities, +}; #[derive(Debug, Clone, PartialEq, Properties)] pub struct PromptScreenTestProps { @@ -47,30 +47,11 @@ pub fn PromptScreenTest( ) -> Html { let options = match prompt { ActionPrompt::WolvesIntro { wolves } => { - let dec_disabled = wolves - .is_empty() - .then_some(String::from("already have zero wolves")); - let dec = { - let current = wolves.len(); + let on_update = { let send = send.clone(); - Callback::from(move |_| { + Callback::from(move |new_count: u8| { let new_prompt = ActionPrompt::WolvesIntro { - wolves: super::identities(current - 1) - .into_iter() - .zip(RoleTitle::ALL.into_iter().filter(|w| w.wolf()).cycle()) - .collect(), - }; - send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0)); - }) - }; - let inc_disabled = - (wolves.len() + 1 > 0xFF).then_some(String::from("already at max wolves")); - let inc = { - let current = wolves.len(); - let send = send.clone(); - Callback::from(move |_| { - let new_prompt = ActionPrompt::WolvesIntro { - wolves: super::identities(current + 1) + wolves: super::identities(new_count as _) .into_iter() .zip(RoleTitle::ALL.into_iter().filter(|w| w.wolf()).cycle()) .collect(), @@ -79,21 +60,13 @@ pub fn PromptScreenTest( }) }; html! { -
- - - -
+ + {" wolves"} + } } ActionPrompt::RoleChange { new_role, .. } => { @@ -194,14 +167,16 @@ pub fn PromptScreenTest( } } - ActionPrompt::MapleWolf { kill_or_die, .. } => { - let toggle = !*kill_or_die; - let on_toggle = { + ActionPrompt::MapleWolf { + nights_til_starvation, + .. + } => { + let on_update = { let send = send.clone(); - Callback::from(move |_| { + Callback::from(move |new_nights: u8| { let new_prompt = ActionPrompt::MapleWolf { character_id: super::identity(), - kill_or_die: toggle, + nights_til_starvation: new_nights, living_players: identities(20), marked: None, }; @@ -209,14 +184,15 @@ pub fn PromptScreenTest( }) }; - let button_text = if toggle { - "turn starving" - } else { - "feed the poor boy" - }; html! {
- + + {" nights"} +
} } @@ -284,75 +260,33 @@ pub fn PromptScreenTest( } } ActionPrompt::MasonsWake { masons, .. } => { - let dec_disabled = - (masons.len() == 1).then_some(String::from("already at min recruits")); - let dec = { - let current = masons.len(); + let on_update = { let send = send.clone(); - Callback::from(move |_| { + Callback::from(move |new_count: u8| { let new_prompt = ActionPrompt::MasonsWake { leader: super::identity(), - masons: super::identities(current.saturating_sub(1)), - }; - send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0)); - }) - }; - let inc_disabled = - (masons.len() > 0xFF).then_some(String::from("already at max wolves")); - let inc = { - let current = masons.len(); - let send = send.clone(); - Callback::from(move |_| { - let new_prompt = ActionPrompt::MasonsWake { - leader: super::identity(), - masons: super::identities(current.saturating_add(1)), + masons: super::identities(new_count as _), }; send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0)); }) }; html! { -
- - - -
+ + {" masons"} + } } ActionPrompt::MasonLeaderRecruit { recruits_left, .. } => { - let dec_disabled = - (recruits_left.get() == 1).then_some(String::from("already at min recruits")); - let dec = { - let current = recruits_left.get(); + let on_update = { let send = send.clone(); - Callback::from(move |_| { + Callback::from(move |new_count: u8| { let new_prompt = ActionPrompt::MasonLeaderRecruit { character_id: super::identity(), - recruits_left: NonZeroU8::new(current.saturating_sub(1)).unwrap(), - potential_recruits: identities(20), - marked: None, - }; - send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0)); - }) - }; - let inc_disabled = - (recruits_left.get() == 0xFF).then_some(String::from("already at max wolves")); - let inc = { - let current = recruits_left.get(); - let send = send.clone(); - Callback::from(move |_| { - let new_prompt = ActionPrompt::MasonLeaderRecruit { - character_id: super::identity(), - recruits_left: NonZeroU8::new(current.saturating_add(1)).unwrap(), + recruits_left: NonZeroU8::new(new_count).unwrap(), potential_recruits: identities(20), marked: None, }; @@ -360,21 +294,13 @@ pub fn PromptScreenTest( }) }; html! { -
- - - -
+ + {" recruits"} + } }