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 {
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,
}),

View File

@ -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(());

View File

@ -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,
},
}),
})),

View File

@ -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<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;
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/";

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 {
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,

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_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<WerewolfError>| cb_clone.send_message(err));
if path.starts_with("/host/test") {
yew::Renderer::<TestClientRemote>::with_root(app_element).render();
} else if path.starts_with("/host") {
if path.starts_with("/host") {
let host = yew::Renderer::<Host>::with_root(app_element).render();
if path.starts_with("/host/big") {
host.send_message(HostEvent::SetBigScreenState(true));

View File

@ -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)}
<MapleWolfPage1 starving={*kill_or_die} />
<MapleWolfPage1 nights_til_starvation={*nights_til_starvation} />
</>
}]),
}]
.into_iter()
.collect(),
ActionPrompt::MasonLeaderRecruit {
character_id,
recruits_left,

View File

@ -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! {
<div>
<h3 class="red">{"YOU ARE STARVING"}</h3>
<h3>{"IF YOU FAIL TO EAT TONIGHT, YOU WILL DIE"}</h3>
</div>
})
.unwrap_or_else(|| {
}
} else {
let nights = if *nights_til_starvation == 1 {
html! {
<h3>
{"IF YOU DON'T EAT FOR TOO LONG, YOU WILL "}
<span class="red">{"STARVE"}</span>
</h3>
<>
<span class="red">{"TOMORROW NIGHT "}</span>
</>
}
});
} 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! {
<div class="role-page">
<h1 class="offensive">{"MAPLE WOLF"}</h1>
@ -48,7 +66,7 @@ pub fn MapleWolfPage1(MapleWolfPage1Props { starving }: &MapleWolfPage1Props) ->
<div class="info-icon-grow">
<Icon source={IconSource::MapleWolf} icon_type={IconType::Fit}/>
</div>
{starving}
{food_state}
</div>
</div>
}

View File

@ -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<ActionPromptTitle> 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,
},

View File

@ -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! {
<div class="prompt-number">
<Button
on_click={dec}
disabled_reason={dec_disabled}
>
{"decrease"}
</Button>
<label>{wolves.len()}{" wolves"}</label>
<Button
on_click={inc}
disabled_reason={inc_disabled}
>
{"increase"}
</Button>
</div>
<IncDecU8
value={wolves.len() as u8}
value_range={1..0xFF}
on_update={on_update}
>
{" wolves"}
</IncDecU8>
}
}
ActionPrompt::RoleChange { new_role, .. } => {
@ -194,14 +167,16 @@ pub fn PromptScreenTest(
</div>
}
}
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! {
<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>
}
}
@ -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! {
<div class="prompt-number">
<Button
on_click={dec}
disabled_reason={dec_disabled}
>
{"decrease"}
</Button>
<label>{masons.len()}{" masons"}</label>
<Button
on_click={inc}
disabled_reason={inc_disabled}
>
{"increase"}
</Button>
</div>
<IncDecU8
value={masons.len() as u8}
value_range={1..0xFF}
on_update={on_update}
>
{" masons"}
</IncDecU8>
}
}
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! {
<div class="prompt-number">
<Button
on_click={dec}
disabled_reason={dec_disabled}
>
{"decrease"}
</Button>
<label>{recruits_left.get()}{" recruits"}</label>
<Button
on_click={inc}
disabled_reason={inc_disabled}
>
{"increase"}
</Button>
</div>
<IncDecU8
value={recruits_left.get()}
value_range={1..0xFF}
on_update={on_update}
>
{" recruits"}
</IncDecU8>
}
}