skip prompt button
This commit is contained in:
parent
2b704ddff6
commit
060180579d
|
|
@ -171,6 +171,10 @@ impl Game {
|
||||||
settings: village.settings(),
|
settings: village.settings(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
(GameState::Night { night }, HostGameMessage::Night(HostNightMessage::SkipAction)) => {
|
||||||
|
night.skip_action()?;
|
||||||
|
self.process(HostGameMessage::GetState)
|
||||||
|
}
|
||||||
(GameState::Night { night }, HostGameMessage::GetState) => {
|
(GameState::Night { night }, HostGameMessage::GetState) => {
|
||||||
if let Some(res) = night.current_result() {
|
if let Some(res) = night.current_result() {
|
||||||
return Ok(ServerToHostMessage::ActionResult(
|
return Ok(ServerToHostMessage::ActionResult(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
fn remove_reverted_prompts(
|
||||||
mut action_queue: VecDeque<ActionPrompt>,
|
mut action_queue: VecDeque<ActionPrompt>,
|
||||||
reverting: CharacterId,
|
reverting: CharacterId,
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ pub struct NightChoice {
|
||||||
|
|
||||||
impl NightChoice {
|
impl NightChoice {
|
||||||
pub fn new(prompt: ActionPrompt, result: ActionResult, village: &Village) -> Option<Self> {
|
pub fn new(prompt: ActionPrompt, result: ActionResult, village: &Village) -> Option<Self> {
|
||||||
Some(Self {
|
(result != ActionResult::SkippedByHost).then_some(Self {
|
||||||
prompt: StoryActionPrompt::new(prompt, village)?,
|
prompt: StoryActionPrompt::new(prompt, village)?,
|
||||||
result: StoryActionResult::new(result),
|
result: StoryActionResult::new(result),
|
||||||
})
|
})
|
||||||
|
|
@ -107,6 +107,7 @@ pub enum StoryActionResult {
|
||||||
impl StoryActionResult {
|
impl StoryActionResult {
|
||||||
pub fn new(result: ActionResult) -> Option<Self> {
|
pub fn new(result: ActionResult) -> Option<Self> {
|
||||||
Some(match result {
|
Some(match result {
|
||||||
|
ActionResult::SkippedByHost => return None,
|
||||||
ActionResult::ShiftFailed => Self::ShiftFailed,
|
ActionResult::ShiftFailed => Self::ShiftFailed,
|
||||||
ActionResult::BeholderSawNothing => Self::BeholderSawNothing,
|
ActionResult::BeholderSawNothing => Self::BeholderSawNothing,
|
||||||
ActionResult::BeholderSawEverything => Self::BeholderSawEverything,
|
ActionResult::BeholderSawEverything => Self::BeholderSawEverything,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ mod night_order;
|
||||||
mod previous;
|
mod previous;
|
||||||
mod revert;
|
mod revert;
|
||||||
mod role;
|
mod role;
|
||||||
|
mod skip;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
character::{Character, CharacterId},
|
character::{Character, CharacterId},
|
||||||
|
|
@ -339,9 +340,30 @@ pub trait GameExt {
|
||||||
fn next_expect_game_over(&mut self) -> GameOver;
|
fn next_expect_game_over(&mut self) -> GameOver;
|
||||||
fn prev(&mut self) -> ServerToHostMessage;
|
fn prev(&mut self) -> ServerToHostMessage;
|
||||||
fn mark_villager(&mut self) -> ActionPrompt;
|
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 {
|
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 {
|
fn prev(&mut self) -> ServerToHostMessage {
|
||||||
self.process(HostGameMessage::PreviousState).unwrap()
|
self.process(HostGameMessage::PreviousState).unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
|
}
|
||||||
|
|
@ -60,6 +60,7 @@ pub enum HostNightMessage {
|
||||||
ActionResponse(ActionResponse),
|
ActionResponse(ActionResponse),
|
||||||
Next,
|
Next,
|
||||||
NextPage,
|
NextPage,
|
||||||
|
SkipAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -623,6 +623,7 @@ pub enum ActionResult {
|
||||||
GoBackToSleep,
|
GoBackToSleep,
|
||||||
ShiftFailed,
|
ShiftFailed,
|
||||||
Continue,
|
Continue,
|
||||||
|
SkippedByHost,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionResult {
|
impl ActionResult {
|
||||||
|
|
@ -660,7 +661,8 @@ impl ActionResult {
|
||||||
| ActionResult::Mortician(_, _)
|
| ActionResult::Mortician(_, _)
|
||||||
| ActionResult::Insomniac(_)
|
| ActionResult::Insomniac(_)
|
||||||
| ActionResult::GoBackToSleep
|
| ActionResult::GoBackToSleep
|
||||||
| ActionResult::Continue => return None,
|
| ActionResult::Continue
|
||||||
|
| ActionResult::SkippedByHost => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -560,12 +560,40 @@ impl Component for Host {
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => 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(|| {
|
let nav = self.big_screen.not().then(|| {
|
||||||
html! {
|
html! {
|
||||||
<nav class="host-nav" style="z-index: 3;">
|
<nav class="host-nav" style="z-index: 3;">
|
||||||
{previous_btn}
|
{previous_btn}
|
||||||
{view_roles_btn}
|
{view_roles_btn}
|
||||||
{override_screens_btn}
|
{override_screens_btn}
|
||||||
|
{skip_btn}
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
|
||||||
</CoverOfDarkness>
|
</CoverOfDarkness>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
ActionResult::Continue => {
|
ActionResult::SkippedByHost | ActionResult::Continue => {
|
||||||
props.on_complete.emit(HostMessage::GetState);
|
props.on_complete.emit(HostMessage::GetState);
|
||||||
return html! {
|
return html! {
|
||||||
<CoverOfDarkness />
|
<CoverOfDarkness />
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,7 @@ pub enum TestScreen {
|
||||||
impl From<ActionResultTitle> for TestScreen {
|
impl From<ActionResultTitle> for TestScreen {
|
||||||
fn from(value: ActionResultTitle) -> Self {
|
fn from(value: ActionResultTitle) -> Self {
|
||||||
TestScreen::Result(match value {
|
TestScreen::Result(match value {
|
||||||
|
ActionResultTitle::SkippedByHost => ActionResult::SkippedByHost,
|
||||||
ActionResultTitle::RoleBlocked => ActionResult::RoleBlocked,
|
ActionResultTitle::RoleBlocked => ActionResult::RoleBlocked,
|
||||||
ActionResultTitle::Drunk => ActionResult::Drunk,
|
ActionResultTitle::Drunk => ActionResult::Drunk,
|
||||||
ActionResultTitle::Seer => ActionResult::Seer(identity(), Alignment::Village),
|
ActionResultTitle::Seer => ActionResult::Seer(identity(), Alignment::Village),
|
||||||
|
|
@ -421,7 +422,9 @@ fn result_class(result: &ActionResult) -> Option<&'static str> {
|
||||||
| ActionResultTitle::BeholderSawEverything
|
| ActionResultTitle::BeholderSawEverything
|
||||||
| ActionResultTitle::Seer => Some("intel"),
|
| ActionResultTitle::Seer => Some("intel"),
|
||||||
ActionResultTitle::ShiftFailed => Some("wolves"),
|
ActionResultTitle::ShiftFailed => Some("wolves"),
|
||||||
ActionResultTitle::GoBackToSleep | ActionResultTitle::Continue => None,
|
ActionResultTitle::GoBackToSleep
|
||||||
|
| ActionResultTitle::Continue
|
||||||
|
| ActionResultTitle::SkippedByHost => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ pub fn ResultScreenTest(
|
||||||
| ActionResult::ShiftFailed
|
| ActionResult::ShiftFailed
|
||||||
| ActionResult::Continue
|
| ActionResult::Continue
|
||||||
| ActionResult::Drunk
|
| ActionResult::Drunk
|
||||||
| ActionResult::RoleBlocked => html! {},
|
| ActionResult::RoleBlocked
|
||||||
|
| ActionResult::SkippedByHost => html! {},
|
||||||
ActionResult::Seer(target, alignment) => {
|
ActionResult::Seer(target, alignment) => {
|
||||||
let all = Alignment::ALL
|
let all = Alignment::ALL
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue