skip prompt button

This commit is contained in:
emilis 2026-02-01 22:58:15 +00:00
parent 2b704ddff6
commit 060180579d
No known key found for this signature in database
11 changed files with 162 additions and 5 deletions

View File

@ -171,6 +171,10 @@ impl Game {
settings: village.settings(),
})
}
(GameState::Night { night }, HostGameMessage::Night(HostNightMessage::SkipAction)) => {
night.skip_action()?;
self.process(HostGameMessage::GetState)
}
(GameState::Night { night }, HostGameMessage::GetState) => {
if let Some(res) = night.current_result() {
return Ok(ServerToHostMessage::ActionResult(

View File

@ -421,6 +421,23 @@ impl Night {
})
}
pub fn skip_action(&mut self) -> Result<()> {
match &mut self.night_state {
NightState::Active {
current_result,
current_changes,
..
} => {
*current_result = CurrentResult::GoBackToSleepAfterShown {
result_with_data: ActionResult::SkippedByHost,
};
current_changes.clear();
}
NightState::Complete => return Err(GameError::NightOver),
}
self.next()
}
fn remove_reverted_prompts(
mut action_queue: VecDeque<ActionPrompt>,
reverting: CharacterId,

View File

@ -69,7 +69,7 @@ pub struct NightChoice {
impl NightChoice {
pub fn new(prompt: ActionPrompt, result: ActionResult, village: &Village) -> Option<Self> {
Some(Self {
(result != ActionResult::SkippedByHost).then_some(Self {
prompt: StoryActionPrompt::new(prompt, village)?,
result: StoryActionResult::new(result),
})
@ -107,6 +107,7 @@ pub enum StoryActionResult {
impl StoryActionResult {
pub fn new(result: ActionResult) -> Option<Self> {
Some(match result {
ActionResult::SkippedByHost => return None,
ActionResult::ShiftFailed => Self::ShiftFailed,
ActionResult::BeholderSawNothing => Self::BeholderSawNothing,
ActionResult::BeholderSawEverything => Self::BeholderSawEverything,

View File

@ -18,6 +18,7 @@ mod night_order;
mod previous;
mod revert;
mod role;
mod skip;
use crate::{
character::{Character, CharacterId},
@ -339,9 +340,30 @@ pub trait GameExt {
fn next_expect_game_over(&mut self) -> GameOver;
fn prev(&mut self) -> ServerToHostMessage;
fn mark_villager(&mut self) -> ActionPrompt;
fn skip(&mut self) -> ActionPrompt;
fn skip_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
}
impl GameExt for Game {
fn skip(&mut self) -> ActionPrompt {
self.process(HostGameMessage::Night(HostNightMessage::SkipAction))
.unwrap()
.prompt()
}
fn skip_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) {
match self
.process(HostGameMessage::Night(HostNightMessage::SkipAction))
.unwrap()
{
ServerToHostMessage::Daytime {
characters,
marked,
day,
..
} => (characters, marked, day),
other => panic!("expected daytime, got {other:?}"),
}
}
fn prev(&mut self) -> ServerToHostMessage {
self.process(HostGameMessage::PreviousState).unwrap()
}

View File

@ -0,0 +1,78 @@
// Copyright (C) 2026 Emilis Bliūdžius
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#[allow(unused)]
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
use crate::{
game::{Game, GameSettings, SetupRole},
game_test::{
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
},
message::night::ActionPromptTitle,
};
#[test]
pub fn skip_wolf_kill_all_villagers() {
init_log();
let players = gen_players(1..21);
let mut player_ids = players.iter().map(|p| p.player_id);
let wolf = player_ids.next().unwrap();
let mut settings = GameSettings::empty();
settings.add_and_assign(SetupRole::Werewolf, wolf);
settings.fill_remaining_slots_with_villagers(players.len());
let mut game = Game::new(&players, settings).unwrap();
game.r#continue().r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
game.r#continue().sleep();
game.next_expect_day();
game.execute().title().wolf_pack_kill();
game.skip_expect_day();
assert!(game.village().characters().into_iter().all(|c| c.alive()))
}
#[test]
pub fn skip_wolf_kill_has_seer() {
init_log();
let players = gen_players(1..21);
let mut player_ids = players.iter().map(|p| p.player_id);
let wolf = player_ids.next().unwrap();
let seer = player_ids.next().unwrap();
let mut settings = GameSettings::empty();
settings.add_and_assign(SetupRole::Werewolf, wolf);
settings.add_and_assign(SetupRole::Seer, seer);
settings.fill_remaining_slots_with_villagers(players.len());
let mut game = Game::new(&players, settings).unwrap();
game.r#continue().r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
game.r#continue().sleep();
game.next().title().seer();
game.mark(game.character_by_player_id(wolf).character_id());
game.r#continue();
game.r#continue().sleep();
game.next_expect_day();
game.execute().title().wolf_pack_kill();
game.skip().title().seer();
game.mark(game.character_by_player_id(wolf).character_id());
game.r#continue();
game.r#continue().sleep();
game.next_expect_day();
assert!(game.village().characters().into_iter().all(|c| c.alive()))
}

View File

@ -60,6 +60,7 @@ pub enum HostNightMessage {
ActionResponse(ActionResponse),
Next,
NextPage,
SkipAction,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]

View File

@ -623,6 +623,7 @@ pub enum ActionResult {
GoBackToSleep,
ShiftFailed,
Continue,
SkippedByHost,
}
impl ActionResult {
@ -660,7 +661,8 @@ impl ActionResult {
| ActionResult::Mortician(_, _)
| ActionResult::Insomniac(_)
| ActionResult::GoBackToSleep
| ActionResult::Continue => return None,
| ActionResult::Continue
| ActionResult::SkippedByHost => return None,
})
}
}

View File

@ -560,12 +560,40 @@ impl Component for Host {
}
_ => None,
};
let skip_btn = match &self.state {
HostState::Prompt(_, _) | HostState::Result(_, _) => {
let on_skip_click = callback::send_message(
HostMessage::InGame(HostGameMessage::Night(HostNightMessage::SkipAction)),
self.send.clone(),
);
let on_skip_click = {
Callback::from(move |_| {
on_skip_click.emit(());
crate::components::modal::close_modal_by_id("skip-button");
})
};
Some(html! {
<crate::components::modal::Dialog
id={"skip-button"}
button={html!{{"skip"}}}
close_button=false
>
<h2>{"skip the current prompt?"}</h2>
<p>{"if this is the final prompt of the night, you may not be able to go back"}</p>
<Button on_click={on_skip_click}>{"skip prompt"}</Button>
</crate::components::modal::Dialog>
})
}
_ => None,
};
let nav = self.big_screen.not().then(|| {
html! {
<nav class="host-nav" style="z-index: 3;">
{previous_btn}
{view_roles_btn}
{override_screens_btn}
{skip_btn}
</nav>
}
});

View File

@ -124,7 +124,7 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
</CoverOfDarkness>
};
}
ActionResult::Continue => {
ActionResult::SkippedByHost | ActionResult::Continue => {
props.on_complete.emit(HostMessage::GetState);
return html! {
<CoverOfDarkness />

View File

@ -223,6 +223,7 @@ pub enum TestScreen {
impl From<ActionResultTitle> for TestScreen {
fn from(value: ActionResultTitle) -> Self {
TestScreen::Result(match value {
ActionResultTitle::SkippedByHost => ActionResult::SkippedByHost,
ActionResultTitle::RoleBlocked => ActionResult::RoleBlocked,
ActionResultTitle::Drunk => ActionResult::Drunk,
ActionResultTitle::Seer => ActionResult::Seer(identity(), Alignment::Village),
@ -421,7 +422,9 @@ fn result_class(result: &ActionResult) -> Option<&'static str> {
| ActionResultTitle::BeholderSawEverything
| ActionResultTitle::Seer => Some("intel"),
ActionResultTitle::ShiftFailed => Some("wolves"),
ActionResultTitle::GoBackToSleep | ActionResultTitle::Continue => None,
ActionResultTitle::GoBackToSleep
| ActionResultTitle::Continue
| ActionResultTitle::SkippedByHost => None,
}
}

View File

@ -48,7 +48,8 @@ pub fn ResultScreenTest(
| ActionResult::ShiftFailed
| ActionResult::Continue
| ActionResult::Drunk
| ActionResult::RoleBlocked => html! {},
| ActionResult::RoleBlocked
| ActionResult::SkippedByHost => html! {},
ActionResult::Seer(target, alignment) => {
let all = Alignment::ALL
.into_iter()