maple wolf: show nights til starving

This commit is contained in:
emilis 2025-11-18 14:35:35 +00:00
parent 8e1d2e34d0
commit 9b19b51581
No known key found for this signature in database
14 changed files with 210 additions and 236 deletions

View File

@ -482,7 +482,8 @@ impl Character {
}), }),
Role::MapleWolf { last_kill_on_night } => prompts.push(ActionPrompt::MapleWolf { Role::MapleWolf { last_kill_on_night } => prompts.push(ActionPrompt::MapleWolf {
character_id: self.identity(), 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()), living_players: village.living_players_excluding(self.character_id()),
marked: None, marked: None,
}), }),

View File

@ -108,13 +108,13 @@ impl Night {
Some(( Some((
ActionPrompt::MapleWolf { ActionPrompt::MapleWolf {
character_id, character_id,
kill_or_die, nights_til_starvation,
marked, marked,
.. ..
}, },
_, _,
)) => { )) => {
if *kill_or_die { if *nights_til_starvation == 0 {
(character_id.character_id, *marked) (character_id.character_id, *marked)
} else { } else {
return Ok(()); return Ok(());

View File

@ -238,8 +238,8 @@ impl Night {
} }
ActionPrompt::MapleWolf { ActionPrompt::MapleWolf {
character_id, character_id,
kill_or_die,
marked: Some(marked), marked: Some(marked),
nights_til_starvation,
.. ..
} => Ok(ResponseOutcome::ActionComplete(ActionComplete { } => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep, result: ActionResult::GoBackToSleep,
@ -248,7 +248,7 @@ impl Night {
died_to: DiedTo::MapleWolf { died_to: DiedTo::MapleWolf {
night, night,
source: character_id.character_id, source: character_id.character_id,
starves_if_fails: *kill_or_die, starves_if_fails: *nights_til_starvation == 0,
}, },
}), }),
})), })),

View File

@ -111,7 +111,7 @@ pub enum ActionPrompt {
#[checks(ActionType::Other)] #[checks(ActionType::Other)]
MapleWolf { MapleWolf {
character_id: CharacterIdentity, character_id: CharacterIdentity,
kill_or_die: bool, nights_til_starvation: u8,
living_players: Box<[CharacterIdentity]>, living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>, marked: Option<CharacterId>,
}, },

View File

@ -2412,3 +2412,14 @@ li.choice {
} }
} }
} }
.inc-dec {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 20px;
.inc-dec-content {
flex-grow: 1;
}
}

View File

@ -21,8 +21,6 @@ pub mod host {
mod host; mod host;
pub use host::*; pub use host::*;
} }
pub mod test_remote;
// mod socket;
const DEBUG_URL: &str = "ws://192.168.1.162:8080/connect/"; const DEBUG_URL: &str = "ws://192.168.1.162:8080/connect/";
const LIVE_URL: &str = "wss://wolf.emilis.dev/connect/"; const LIVE_URL: &str = "wss://wolf.emilis.dev/connect/";

View File

@ -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 <https://www.gnu.org/licenses/>.
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! {
<TestScreens send={send_cb} />
}
}

View File

@ -375,14 +375,14 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
), ),
ActionPrompt::MapleWolf { ActionPrompt::MapleWolf {
character_id, character_id,
kill_or_die, nights_til_starvation,
living_players, living_players,
marked, marked,
} => ( } => (
Some(character_id), Some(character_id),
living_players, living_players,
marked.iter().cloned().collect(), 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 { ActionPrompt::WolfPackKill {
living_villagers, living_villagers,

View File

@ -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 <https://www.gnu.org/licenses/>.
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<u8>,
pub on_update: Callback<u8>,
}
#[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! {
<div class={classes!("inc-dec", class.clone())}>
<Button
on_click={dec}
disabled_reason={dec_disabled}
classes={button_class.clone()}
>
{"decrease"}
</Button>
<div class="inc-dec-content">
<span class="inc-dec-count">{*value_state}</span>
{children.clone()}
</div>
<Button
on_click={inc}
disabled_reason={inc_disabled}
classes={button_class.clone()}
>
{"increase"}
</Button>
</div>
}
}

View File

@ -46,13 +46,9 @@ const BUILD_ID_LONG: &str = werewolves_macros::build_id_long!();
const BUILD_DIRTY: bool = werewolves_macros::build_dirty!(); const BUILD_DIRTY: bool = werewolves_macros::build_dirty!();
const BUILD_TIME: &str = werewolves_macros::build_time!(); const BUILD_TIME: &str = werewolves_macros::build_time!();
use crate::{ use crate::clients::{
clients::{ client::{Client2, ClientContext},
client::{Client2, ClientContext}, host::{Host, HostEvent},
host::{Host, HostEvent},
test_remote::TestClientRemote,
},
test_util::TestScreens,
}; };
fn main() { fn main() {
@ -70,9 +66,7 @@ fn main() {
let error_callback = let error_callback =
Callback::from(move |err: Option<WerewolfError>| cb_clone.send_message(err)); Callback::from(move |err: Option<WerewolfError>| cb_clone.send_message(err));
if path.starts_with("/host/test") { if path.starts_with("/host") {
yew::Renderer::<TestClientRemote>::with_root(app_element).render();
} else if path.starts_with("/host") {
let host = yew::Renderer::<Host>::with_root(app_element).render(); let host = yew::Renderer::<Host>::with_root(app_element).render();
if path.starts_with("/host/big") { if path.starts_with("/host/big") {
host.send_message(HostEvent::SetBigScreenState(true)); host.send_message(HostEvent::SetBigScreenState(true));

View File

@ -109,14 +109,16 @@ impl RolePage for ActionPrompt {
}]), }]),
ActionPrompt::MapleWolf { ActionPrompt::MapleWolf {
character_id, character_id,
kill_or_die, nights_til_starvation,
.. ..
} => Rc::new([html! { } => [html! {
<> <>
{ident(character_id)} {ident(character_id)}
<MapleWolfPage1 starving={*kill_or_die} /> <MapleWolfPage1 nights_til_starvation={*nights_til_starvation} />
</> </>
}]), }]
.into_iter()
.collect(),
ActionPrompt::MasonLeaderRecruit { ActionPrompt::MasonLeaderRecruit {
character_id, character_id,
recruits_left, recruits_left,

View File

@ -17,27 +17,45 @@ use yew::prelude::*;
use crate::components::{Icon, IconSource, IconType}; use crate::components::{Icon, IconSource, IconType};
#[derive(Debug, Clone, Copy, PartialEq, Properties)] #[derive(Debug, Clone, Copy, PartialEq, Properties)]
pub struct MapleWolfPage1Props { pub struct MapleWolfPageProps {
pub starving: bool, pub nights_til_starvation: u8,
} }
#[function_component] #[function_component]
pub fn MapleWolfPage1(MapleWolfPage1Props { starving }: &MapleWolfPage1Props) -> Html { pub fn MapleWolfPage1(
let starving = starving MapleWolfPageProps {
.then_some(html! { nights_til_starvation,
}: &MapleWolfPageProps,
) -> Html {
let food_state = if *nights_til_starvation == 0 {
html! {
<div> <div>
<h3 class="red">{"YOU ARE STARVING"}</h3> <h3 class="red">{"YOU ARE STARVING"}</h3>
<h3>{"IF YOU FAIL TO EAT TONIGHT, YOU WILL DIE"}</h3> <h3>{"IF YOU FAIL TO EAT TONIGHT, YOU WILL DIE"}</h3>
</div> </div>
}) }
.unwrap_or_else(|| { } else {
let nights = if *nights_til_starvation == 1 {
html! { html! {
<h3> <>
{"IF YOU DON'T EAT FOR TOO LONG, YOU WILL "} <span class="red">{"TOMORROW NIGHT "}</span>
<span class="red">{"STARVE"}</span> </>
</h3>
} }
}); } else {
html! {
<>
{"IN "}<span class="red">{*nights_til_starvation}</span>{" NIGHTS "}
</>
}
};
html! {
<h3>
{"IF YOU FAIL TO EAT "}
{nights}
{"YOU WILL "}<span class="yellow">{"STARVE"}</span>
</h3>
}
};
html! { html! {
<div class="role-page"> <div class="role-page">
<h1 class="offensive">{"MAPLE WOLF"}</h1> <h1 class="offensive">{"MAPLE WOLF"}</h1>
@ -48,7 +66,7 @@ pub fn MapleWolfPage1(MapleWolfPage1Props { starving }: &MapleWolfPage1Props) ->
<div class="info-icon-grow"> <div class="info-icon-grow">
<Icon source={IconSource::MapleWolf} icon_type={IconType::Fit}/> <Icon source={IconSource::MapleWolf} icon_type={IconType::Fit}/>
</div> </div>
{starving} {food_state}
</div> </div>
</div> </div>
} }

View File

@ -21,10 +21,8 @@ use werewolves_proto::{
diedto::DiedToTitle, diedto::DiedToTitle,
message::{ message::{
CharacterIdentity, CharacterIdentity,
host::{HostMessage, ServerToHostMessage}, host::ServerToHostMessage,
night::{ night::{ActionPrompt, ActionPromptTitle, ActionResult, ActionResultTitle, Visits},
ActionPrompt, ActionPromptTitle, ActionResult, ActionResultTitle, ActionType, Visits,
},
}, },
role::{Alignment, AlignmentEq, Killer, Powerful, RoleTitle}, role::{Alignment, AlignmentEq, Killer, Powerful, RoleTitle},
}; };
@ -304,7 +302,7 @@ impl From<ActionPromptTitle> for TestScreen {
}, },
ActionPromptTitle::MapleWolf => ActionPrompt::MapleWolf { ActionPromptTitle::MapleWolf => ActionPrompt::MapleWolf {
character_id: identities(1).into_iter().next().unwrap(), character_id: identities(1).into_iter().next().unwrap(),
kill_or_die: false, nights_til_starvation: 0,
living_players: identities(20), living_players: identities(20),
marked: None, marked: None,
}, },

View File

@ -18,15 +18,15 @@ use core::num::NonZeroU8;
use web_sys::HtmlSelectElement; use web_sys::HtmlSelectElement;
use werewolves_proto::{ use werewolves_proto::{
message::{ message::{host::ServerToHostMessage, night::ActionPrompt},
host::{HostGameMessage, HostMessage, HostNightMessage, ServerToHostMessage},
night::ActionPrompt,
},
player::RoleChange,
role::{PreviousGuardianAction, RoleTitle}, 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)] #[derive(Debug, Clone, PartialEq, Properties)]
pub struct PromptScreenTestProps { pub struct PromptScreenTestProps {
@ -47,30 +47,11 @@ pub fn PromptScreenTest(
) -> Html { ) -> Html {
let options = match prompt { let options = match prompt {
ActionPrompt::WolvesIntro { wolves } => { ActionPrompt::WolvesIntro { wolves } => {
let dec_disabled = wolves let on_update = {
.is_empty()
.then_some(String::from("already have zero wolves"));
let dec = {
let current = wolves.len();
let send = send.clone(); let send = send.clone();
Callback::from(move |_| { Callback::from(move |new_count: u8| {
let new_prompt = ActionPrompt::WolvesIntro { 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(),
};
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)
.into_iter() .into_iter()
.zip(RoleTitle::ALL.into_iter().filter(|w| w.wolf()).cycle()) .zip(RoleTitle::ALL.into_iter().filter(|w| w.wolf()).cycle())
.collect(), .collect(),
@ -79,21 +60,13 @@ pub fn PromptScreenTest(
}) })
}; };
html! { html! {
<div class="prompt-number"> <IncDecU8
<Button value={wolves.len() as u8}
on_click={dec} value_range={1..0xFF}
disabled_reason={dec_disabled} on_update={on_update}
> >
{"decrease"} {" wolves"}
</Button> </IncDecU8>
<label>{wolves.len()}{" wolves"}</label>
<Button
on_click={inc}
disabled_reason={inc_disabled}
>
{"increase"}
</Button>
</div>
} }
} }
ActionPrompt::RoleChange { new_role, .. } => { ActionPrompt::RoleChange { new_role, .. } => {
@ -194,14 +167,16 @@ pub fn PromptScreenTest(
</div> </div>
} }
} }
ActionPrompt::MapleWolf { kill_or_die, .. } => { ActionPrompt::MapleWolf {
let toggle = !*kill_or_die; nights_til_starvation,
let on_toggle = { ..
} => {
let on_update = {
let send = send.clone(); let send = send.clone();
Callback::from(move |_| { Callback::from(move |new_nights: u8| {
let new_prompt = ActionPrompt::MapleWolf { let new_prompt = ActionPrompt::MapleWolf {
character_id: super::identity(), character_id: super::identity(),
kill_or_die: toggle, nights_til_starvation: new_nights,
living_players: identities(20), living_players: identities(20),
marked: None, marked: None,
}; };
@ -209,14 +184,15 @@ pub fn PromptScreenTest(
}) })
}; };
let button_text = if toggle {
"turn starving"
} else {
"feed the poor boy"
};
html! { html! {
<div class="prompt-options"> <div class="prompt-options">
<Button on_click={on_toggle}>{button_text}</Button> <IncDecU8
value={*nights_til_starvation}
value_range={0..0xFF}
on_update={on_update}
>
{" nights"}
</IncDecU8>
</div> </div>
} }
} }
@ -284,75 +260,33 @@ pub fn PromptScreenTest(
} }
} }
ActionPrompt::MasonsWake { masons, .. } => { ActionPrompt::MasonsWake { masons, .. } => {
let dec_disabled = let on_update = {
(masons.len() == 1).then_some(String::from("already at min recruits"));
let dec = {
let current = masons.len();
let send = send.clone(); let send = send.clone();
Callback::from(move |_| { Callback::from(move |new_count: u8| {
let new_prompt = ActionPrompt::MasonsWake { let new_prompt = ActionPrompt::MasonsWake {
leader: super::identity(), leader: super::identity(),
masons: super::identities(current.saturating_sub(1)), masons: super::identities(new_count as _),
};
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)),
}; };
send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0)); send.emit(ServerToHostMessage::ActionPrompt(new_prompt.clone(), 0));
}) })
}; };
html! { html! {
<div class="prompt-number"> <IncDecU8
<Button value={masons.len() as u8}
on_click={dec} value_range={1..0xFF}
disabled_reason={dec_disabled} on_update={on_update}
> >
{"decrease"} {" masons"}
</Button> </IncDecU8>
<label>{masons.len()}{" masons"}</label>
<Button
on_click={inc}
disabled_reason={inc_disabled}
>
{"increase"}
</Button>
</div>
} }
} }
ActionPrompt::MasonLeaderRecruit { recruits_left, .. } => { ActionPrompt::MasonLeaderRecruit { recruits_left, .. } => {
let dec_disabled = let on_update = {
(recruits_left.get() == 1).then_some(String::from("already at min recruits"));
let dec = {
let current = recruits_left.get();
let send = send.clone(); let send = send.clone();
Callback::from(move |_| { Callback::from(move |new_count: u8| {
let new_prompt = ActionPrompt::MasonLeaderRecruit { let new_prompt = ActionPrompt::MasonLeaderRecruit {
character_id: super::identity(), character_id: super::identity(),
recruits_left: NonZeroU8::new(current.saturating_sub(1)).unwrap(), recruits_left: NonZeroU8::new(new_count).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(),
potential_recruits: identities(20), potential_recruits: identities(20),
marked: None, marked: None,
}; };
@ -360,21 +294,13 @@ pub fn PromptScreenTest(
}) })
}; };
html! { html! {
<div class="prompt-number"> <IncDecU8
<Button value={recruits_left.get()}
on_click={dec} value_range={1..0xFF}
disabled_reason={dec_disabled} on_update={on_update}
> >
{"decrease"} {" recruits"}
</Button> </IncDecU8>
<label>{recruits_left.get()}{" recruits"}</label>
<Button
on_click={inc}
disabled_reason={inc_disabled}
>
{"increase"}
</Button>
</div>
} }
} }