refactored night actions a la day markings

This commit is contained in:
emilis 2025-10-03 22:47:38 +01:00
parent cbeee94113
commit 5ad2831688
No known key found for this signature in database
15 changed files with 1331 additions and 1052 deletions

View File

@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize};
use crate::{
error::GameError,
game::night::Night,
game::night::{Night, ServerAction},
message::{
CharacterState, Identification,
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
@ -133,7 +133,8 @@ impl Game {
GameState::Night { night },
HostGameMessage::Night(HostNightMessage::ActionResponse(resp)),
) => match night.received_response(resp.clone()) {
Ok(res) => Ok(ServerToHostMessage::ActionResult(
Ok(ServerAction::Prompt(prompt)) => Ok(ServerToHostMessage::ActionPrompt(prompt)),
Ok(ServerAction::Result(res)) => Ok(ServerToHostMessage::ActionResult(
night.current_character().map(|c| c.identity()),
res,
)),

File diff suppressed because it is too large Load Diff

View File

@ -68,6 +68,28 @@ impl Village {
})
}
pub fn killing_wolf(&self) -> Option<&Character> {
let wolves = self.characters.iter().filter(|c| c.is_wolf());
{
let ww = wolves
.clone()
.filter(|w| matches!(w.role().title(), RoleTitle::Werewolf))
.collect::<Box<[_]>>();
if !ww.is_empty() {
return Some(ww[rand::random_range(0..ww.len())]);
}
}
{
let wolves = wolves.collect::<Box<[_]>>();
if wolves.is_empty() {
return None;
}
Some(wolves[rand::random_range(0..wolves.len())])
}
}
pub const fn date_time(&self) -> DateTime {
self.date_time
}

View File

@ -5,10 +5,7 @@ use crate::{
game::{Game, GameSettings},
message::{
CharacterState, Identification, PublicIdentity,
host::{
HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage,
ServerToHostMessageTitle,
},
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
},
player::{CharacterId, PlayerId},
@ -20,12 +17,39 @@ use core::{num::NonZeroU8, ops::Range};
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
use std::io::Write;
trait ActionResultExt {
fn sleep(&self);
fn r#continue(&self);
}
impl ActionResultExt for ActionResult {
fn sleep(&self) {
assert_eq!(*self, ActionResult::GoBackToSleep)
}
fn r#continue(&self) {
assert_eq!(*self, ActionResult::Continue)
}
}
trait ServerToHostMessageExt {
fn prompt(self) -> ActionPrompt;
fn result(self) -> ActionResult;
fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
}
impl ServerToHostMessageExt for ServerToHostMessage {
fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) {
match self {
Self::Daytime {
characters,
marked,
day,
} => (characters, marked, day),
resp => panic!("expected daytime, got {resp:?}"),
}
}
fn prompt(self) -> ActionPrompt {
match self {
Self::ActionPrompt(prompt) => prompt,
@ -48,7 +72,10 @@ impl ServerToHostMessageExt for ServerToHostMessage {
trait GameExt {
fn next(&mut self) -> ActionPrompt;
fn r#continue(&mut self) -> ActionResult;
fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
fn mark(&mut self, mark: &CharacterId) -> ActionPrompt;
fn mark_and_check(&mut self, mark: &CharacterId, check: impl FnOnce(&ActionPrompt) -> bool);
fn response(&mut self, resp: ActionResponse) -> ActionResult;
fn execute(&mut self) -> ActionPrompt;
fn mark_for_execution(
@ -58,6 +85,29 @@ trait GameExt {
}
impl GameExt for Game {
fn r#continue(&mut self) -> ActionResult {
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::Continue,
)))
.unwrap()
.result()
}
fn mark(&mut self, mark: &CharacterId) -> ActionPrompt {
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::MarkTarget(mark.clone()),
)))
.unwrap()
.prompt()
}
fn mark_and_check(&mut self, mark: &CharacterId, check: impl FnOnce(&ActionPrompt) -> bool) {
let prompt = self.mark(mark);
if !check(&prompt) {
panic!("unexpected prompt: {prompt:?}");
}
}
fn next(&mut self) -> ActionPrompt {
self.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap()
@ -179,7 +229,7 @@ fn no_wolf_kill_n1() {
let mut game = Game::new(&players, settings).unwrap();
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::ClearCoverOfDarkness
ActionResponse::Continue
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
@ -191,7 +241,7 @@ fn no_wolf_kill_n1() {
));
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::WolvesIntroAck
ActionResponse::Continue
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::GoBackToSleep),
@ -214,7 +264,7 @@ fn yes_wolf_kill_n2() {
let mut game = Game::new(&players, settings).unwrap();
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::ClearCoverOfDarkness
ActionResponse::Continue
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
@ -226,7 +276,7 @@ fn yes_wolf_kill_n2() {
));
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::WolvesIntroAck
ActionResponse::Continue
)))
.unwrap()
.result(),
@ -271,7 +321,7 @@ fn yes_wolf_kill_n2() {
);
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::ClearCoverOfDarkness
ActionResponse::Continue
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
@ -281,7 +331,8 @@ fn yes_wolf_kill_n2() {
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap(),
ServerToHostMessage::ActionPrompt(ActionPrompt::WolfPackKill {
living_villagers: _
living_villagers: _,
marked: _,
})
));
}
@ -297,7 +348,7 @@ fn protect_stops_shapeshift() {
let mut game = Game::new(&players, settings).unwrap();
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::ClearCoverOfDarkness
ActionResponse::Continue,
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
@ -309,7 +360,7 @@ fn protect_stops_shapeshift() {
));
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::WolvesIntroAck
ActionResponse::Continue
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::GoBackToSleep),
@ -353,14 +404,7 @@ fn protect_stops_shapeshift() {
.title(),
ActionPromptTitle::CoverOfDarkness
);
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::ClearCoverOfDarkness
)))
.unwrap()
.result(),
ActionResult::Continue
);
game.r#continue().r#continue();
let (prot_and_wolf_target, prot_char_id) = match game
.process(HostGameMessage::Night(HostNightMessage::Next))
@ -369,6 +413,7 @@ fn protect_stops_shapeshift() {
ServerToHostMessage::ActionPrompt(ActionPrompt::Protector {
character_id: prot_char_id,
targets,
marked: None,
}) => (
targets
.into_iter()
@ -388,55 +433,35 @@ fn protect_stops_shapeshift() {
.clone();
log::info!("target: {target:#?}");
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::Protector(prot_and_wolf_target.clone())
match game
.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::MarkTarget(prot_and_wolf_target.clone()),
)))
.unwrap()
.result(),
ActionResult::GoBackToSleep,
);
{
ServerToHostMessage::ActionPrompt(ActionPrompt::Protector {
marked: Some(mark), ..
}) => assert_eq!(mark, prot_and_wolf_target, "marked target"),
resp => panic!("unexpected response: {resp:?}"),
}
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap()
.prompt()
.title(),
ActionPromptTitle::WolfPackKill
);
game.r#continue().sleep();
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::WolfPackKillVote(prot_and_wolf_target.clone())
)))
.unwrap()
.result(),
ActionResult::Continue,
);
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap()
.prompt()
.title(),
ActionPromptTitle::Shapeshifter,
);
game.mark_and_check(&prot_and_wolf_target, |c| match c {
ActionPrompt::WolfPackKill {
marked: Some(mark), ..
} => prot_and_wolf_target == *mark,
_ => false,
});
game.r#continue().r#continue();
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::Shapeshifter(true)
)))
.unwrap()
.result(),
ActionResult::GoBackToSleep,
);
assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter,);
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap()
.title(),
ServerToHostMessageTitle::Daytime,
);
game.response(ActionResponse::Shapeshift);
game.next_expect_day();
let target = game
.village()
@ -462,34 +487,11 @@ fn wolfpack_kill_all_targets_valid() {
settings.add(RoleTitle::Shapeshifter).unwrap();
settings.sub(RoleTitle::Werewolf);
let mut game = Game::new(&players, settings).unwrap();
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::ClearCoverOfDarkness
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::Continue)
);
assert!(matches!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap(),
ServerToHostMessage::ActionPrompt(ActionPrompt::WolvesIntro { wolves: _ })
));
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::WolvesIntroAck
)))
.unwrap(),
ServerToHostMessage::ActionResult(None, ActionResult::GoBackToSleep),
);
assert!(matches!(
game.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap(),
ServerToHostMessage::Daytime {
characters: _,
marked: _,
day: _,
}
));
game.r#continue().r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
game.r#continue().sleep();
game.next_expect_day();
let execution_target = game
.village()
@ -513,28 +515,18 @@ fn wolfpack_kill_all_targets_valid() {
resp => panic!("unexpected server message: {resp:#?}"),
}
assert_eq!(
game.process(HostGameMessage::Day(HostDayMessage::Execute))
.unwrap()
.prompt()
.title(),
ActionPromptTitle::CoverOfDarkness
);
assert_eq!(
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::ClearCoverOfDarkness
)))
.unwrap()
.result(),
ActionResult::Continue
);
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
game.r#continue().r#continue();
let living_villagers = match game
.process(HostGameMessage::Night(HostNightMessage::Next))
.unwrap()
.prompt()
{
ActionPrompt::WolfPackKill { living_villagers } => living_villagers,
ActionPrompt::WolfPackKill {
living_villagers,
marked: _,
} => living_villagers,
_ => panic!("not wolf pack kill"),
};
@ -542,12 +534,14 @@ fn wolfpack_kill_all_targets_valid() {
let mut attempt = game.clone();
if let ServerToHostMessage::Error(GameError::InvalidTarget) = attempt
.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
ActionResponse::WolfPackKillVote(target.character_id.clone()),
ActionResponse::MarkTarget(target.character_id.clone()),
)))
.unwrap()
{
panic!("invalid target {target:?} at index [{idx}]");
}
attempt.r#continue().r#continue();
assert_eq!(attempt.next().title(), ActionPromptTitle::Shapeshifter);
}
}
@ -559,15 +553,9 @@ fn only_1_shapeshift_prompt_if_first_shifts() {
settings.add(RoleTitle::Shapeshifter).unwrap();
settings.sub(RoleTitle::Werewolf);
let mut game = Game::new(&players, settings).unwrap();
assert_eq!(
game.response(ActionResponse::ClearCoverOfDarkness),
ActionResult::Continue
);
game.r#continue().r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
assert_eq!(
game.response(ActionResponse::WolvesIntroAck),
ActionResult::GoBackToSleep
);
game.r#continue().sleep();
game.next_expect_day();
let target = game
.village()
@ -579,10 +567,7 @@ fn only_1_shapeshift_prompt_if_first_shifts() {
let (marked, target_list): (&[CharacterId], &[CharacterId]) = (&marked, &[target]);
assert_eq!(target_list, marked);
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
assert_eq!(
game.response(ActionResponse::ClearCoverOfDarkness),
ActionResult::Continue
);
game.r#continue().r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
let target = game
.village()
@ -590,20 +575,18 @@ fn only_1_shapeshift_prompt_if_first_shifts() {
.into_iter()
.find_map(|c| (c.is_village() && c.alive()).then_some(c.character_id().clone()))
.unwrap();
assert_eq!(
game.response(ActionResponse::WolfPackKillVote(target)),
ActionResult::Continue,
);
game.mark_and_check(&target, |p| match p {
ActionPrompt::WolfPackKill {
marked: Some(t), ..
} => *t == target,
_ => false,
});
game.r#continue().r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter);
assert_eq!(
game.response(ActionResponse::Shapeshifter(true)),
ActionResult::Continue,
);
game.response(ActionResponse::Shapeshift).r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::RoleChange);
assert_eq!(
game.response(ActionResponse::RoleChangeAck),
ActionResult::GoBackToSleep
);
game.r#continue().sleep();
game.next_expect_day();
}

View File

@ -31,9 +31,11 @@ fn night_order() {
character_id: character_identity(),
},
ActionPrompt::WolfPackKill {
marked: None,
living_villagers: Box::new([]),
},
ActionPrompt::Protector {
marked: None,
character_id: character_identity(),
targets: Box::new([]),
},

View File

@ -2,11 +2,14 @@ use serde::{Deserialize, Serialize};
use werewolves_macros::{ChecksAs, Titles};
use crate::{
error::GameError,
message::CharacterIdentity,
player::CharacterId,
role::{Alignment, PreviousGuardianAction, RoleTitle},
};
type Result<T> = core::result::Result<T, GameError>;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
pub enum ActionType {
Cover,
@ -37,7 +40,6 @@ pub enum ActionPrompt {
#[checks(ActionType::Cover)]
CoverOfDarkness,
#[checks(ActionType::WolfPackKill)]
#[checks]
WolvesIntro {
wolves: Box<[(CharacterIdentity, RoleTitle)]>,
},
@ -50,48 +52,57 @@ pub enum ActionPrompt {
Seer {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::Protect)]
Protector {
character_id: CharacterIdentity,
targets: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::Other)]
Arcanist {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: (Option<CharacterId>, Option<CharacterId>),
},
#[checks(ActionType::Other)]
Gravedigger {
character_id: CharacterIdentity,
dead_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::Other)]
Hunter {
character_id: CharacterIdentity,
current_target: Option<CharacterIdentity>,
living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::Other)]
Militia {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::Other)]
MapleWolf {
character_id: CharacterIdentity,
kill_or_die: bool,
living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::Protect)]
Guardian {
character_id: CharacterIdentity,
previous: Option<PreviousGuardianAction>,
living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::WolfPackKill)]
WolfPackKill {
living_villagers: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::OtherWolf)]
Shapeshifter { character_id: CharacterIdentity },
@ -99,15 +110,147 @@ pub enum ActionPrompt {
AlphaWolf {
character_id: CharacterIdentity,
living_villagers: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::Direwolf)]
DireWolf {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
}
impl ActionPrompt {
pub fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> {
let mut prompt = self.clone();
match &mut prompt {
ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. }
| ActionPrompt::Shapeshifter { .. }
| ActionPrompt::CoverOfDarkness => Err(GameError::InvalidMessageForGameState),
ActionPrompt::Guardian {
previous,
living_players,
marked,
..
} => {
if !living_players.iter().any(|c| c.character_id == mark)
|| previous
.as_ref()
.and_then(|p| match p {
PreviousGuardianAction::Protect(_) => None,
PreviousGuardianAction::Guard(c) => Some(c.character_id == mark),
})
.unwrap_or_default()
{
// not in target list OR guarded target previous night
return Err(GameError::InvalidTarget);
}
match marked.as_mut() {
Some(marked_cid) => {
if marked_cid == &mark {
marked.take();
} else {
marked.replace(mark);
}
}
None => {
marked.replace(mark);
}
}
Ok(prompt)
}
ActionPrompt::Arcanist {
living_players: targets,
marked,
..
} => {
if !targets.iter().any(|t| t.character_id == mark) {
return Err(GameError::InvalidTarget);
}
match marked {
(None, Some(m)) | (Some(m), None) => {
if *m == mark {
*marked = (None, None);
} else {
*marked = (Some(m.clone()), Some(mark));
}
}
(None, None) => *marked = (Some(mark), None),
(Some(m1), Some(m2)) => {
if *m1 == mark {
*marked = (Some(m2.clone()), None);
} else if *m2 == mark {
*marked = (Some(m1.clone()), None);
} else {
*marked = (Some(m2.clone()), Some(mark));
}
}
}
Ok(prompt)
}
ActionPrompt::Protector {
targets, marked, ..
}
| ActionPrompt::Seer {
living_players: targets,
marked,
..
}
| ActionPrompt::Gravedigger {
dead_players: targets,
marked,
..
}
| ActionPrompt::Hunter {
living_players: targets,
marked,
..
}
| ActionPrompt::Militia {
living_players: targets,
marked,
..
}
| ActionPrompt::MapleWolf {
living_players: targets,
marked,
..
}
| ActionPrompt::WolfPackKill {
living_villagers: targets,
marked,
..
}
| ActionPrompt::AlphaWolf {
living_villagers: targets,
marked,
..
}
| ActionPrompt::DireWolf {
living_players: targets,
marked,
..
} => {
if !targets.iter().any(|t| t.character_id == mark) {
return Err(GameError::InvalidTarget);
}
if let Some(marked_char) = marked.as_ref()
&& *marked_char == mark
{
marked.take();
} else {
marked.replace(mark);
}
Ok(prompt)
}
}
}
pub const fn is_wolfy(&self) -> bool {
self.action_type().is_wolfy()
|| match self {
@ -126,25 +269,22 @@ impl PartialOrd for ActionPrompt {
}
}
#[derive(Debug, Clone, Serialize, PartialEq, Deserialize, ChecksAs)]
#[derive(Debug, Clone, Serialize, PartialEq, Deserialize)]
pub enum ActionResponse {
Seer(CharacterId),
Arcanist(CharacterId, CharacterId),
Gravedigger(CharacterId),
Hunter(CharacterId),
Militia(Option<CharacterId>),
MapleWolf(Option<CharacterId>),
Guardian(CharacterId),
WolfPackKillVote(CharacterId),
#[checks]
Shapeshifter(bool),
AlphaWolf(Option<CharacterId>),
Direwolf(CharacterId),
Protector(CharacterId),
#[checks]
RoleChangeAck,
WolvesIntroAck,
ClearCoverOfDarkness,
// Seer(CharacterId),
// Arcanist(Option<CharacterId>, Option<CharacterId>),
// Gravedigger(CharacterId),
// Hunter(CharacterId),
// Militia(Option<CharacterId>),
// MapleWolf(Option<CharacterId>),
// Guardian(CharacterId),
// WolfPackKillVote(CharacterId),
// AlphaWolf(Option<CharacterId>),
// Direwolf(CharacterId),
// Protector(CharacterId),
MarkTarget(CharacterId),
Shapeshift,
Continue,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]

View File

@ -8,7 +8,7 @@ use crate::{
game::{DateTime, Village},
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
modifier::Modifier,
role::{MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
role::{Alignment, MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
@ -173,6 +173,19 @@ impl Character {
&self.role
}
pub const fn gravedigger_dig(&self) -> Option<RoleTitle> {
match &self.role {
Role::Shapeshifter {
shifted_into: Some(_),
} => None,
_ => Some(self.role.title()),
}
}
pub const fn alignment(&self) -> Alignment {
self.role.alignment()
}
pub const fn role_mut(&mut self) -> &mut Role {
&mut self.role
}
@ -222,22 +235,26 @@ impl Character {
Role::Seer => ActionPrompt::Seer {
character_id: self.identity(),
living_players: village.living_players_excluding(self.character_id()),
marked: None,
},
Role::Arcanist => ActionPrompt::Arcanist {
character_id: self.identity(),
living_players: village.living_players_excluding(self.character_id()),
marked: (None, None),
},
Role::Protector {
last_protected: Some(last_protected),
} => ActionPrompt::Protector {
character_id: self.identity(),
targets: village.living_players_excluding(last_protected),
marked: None,
},
Role::Protector {
last_protected: None,
} => ActionPrompt::Protector {
character_id: self.identity(),
targets: village.living_players_excluding(self.character_id()),
marked: None,
},
Role::Apprentice(role) => {
let current_night = match village.date_time() {
@ -273,17 +290,21 @@ impl Character {
Role::Militia { targeted: None } => ActionPrompt::Militia {
character_id: self.identity(),
living_players: village.living_players_excluding(self.character_id()),
marked: None,
},
Role::Werewolf => ActionPrompt::WolfPackKill {
living_villagers: village.living_players(),
marked: None,
},
Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf {
character_id: self.identity(),
living_villagers: village.living_players_excluding(self.character_id()),
marked: None,
},
Role::DireWolf => ActionPrompt::DireWolf {
character_id: self.identity(),
living_players: village.living_players(),
marked: None,
},
Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter {
character_id: self.identity(),
@ -291,16 +312,19 @@ impl Character {
Role::Gravedigger => ActionPrompt::Gravedigger {
character_id: self.identity(),
dead_players: village.dead_targets(),
marked: None,
},
Role::Hunter { target } => ActionPrompt::Hunter {
character_id: self.identity(),
current_target: target.as_ref().and_then(|t| village.target_by_id(t)),
living_players: village.living_players_excluding(self.character_id()),
marked: None,
},
Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf {
character_id: self.identity(),
kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night,
living_players: village.living_players_excluding(self.character_id()),
marked: None,
},
Role::Guardian {
last_protected: Some(PreviousGuardianAction::Guard(prev_target)),
@ -308,6 +332,7 @@ impl Character {
character_id: self.identity(),
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
living_players: village.living_players_excluding(&prev_target.character_id),
marked: None,
},
Role::Guardian {
last_protected: Some(PreviousGuardianAction::Protect(prev_target)),
@ -315,6 +340,7 @@ impl Character {
character_id: self.identity(),
previous: Some(PreviousGuardianAction::Protect(prev_target.clone())),
living_players: village.living_players(),
marked: None,
},
Role::Guardian {
last_protected: None,
@ -322,6 +348,7 @@ impl Character {
character_id: self.identity(),
previous: None,
living_players: village.living_players(),
marked: None,
},
}))
}

View File

@ -121,7 +121,6 @@ struct Client {
who: String,
sender: Sender<IdentifiedClientMessage>,
receiver: Receiver<ServerMessage>,
// message_history: Vec<ServerMessage>,
}
impl Client {
@ -140,7 +139,6 @@ impl Client {
who,
sender,
receiver,
// message_history: Vec::new(),
}
}
#[cfg(feature = "cbor")]

View File

@ -91,7 +91,7 @@ impl Lobby {
.unwrap()
.next_message()
.await
.expect("get next message");
.expect("get next message"); // TODO: keeps happening
match self.next_inner(msg.clone()).await.map_err(|err| (msg, err)) {
Ok(None) => {}

View File

@ -863,7 +863,8 @@ input {
}
.character-picker {
.character-picker,
.target-picker {
display: flex;
flex-direction: column;
width: 100%;
@ -891,6 +892,16 @@ input {
}
}
&.dead {
$bg: rgba(128, 128, 128, 0.5);
background-color: $bg;
border: 1px solid color.change($bg, $alpha: 1.0);
&:hover {
background-color: color.change($bg, $alpha: 1.0);
}
}
background-color: $village_bg;
border: 1px solid $village_border;

View File

@ -0,0 +1,76 @@
use werewolves_proto::{
message::{CharacterIdentity, PublicIdentity},
player::CharacterId,
};
use yew::prelude::*;
use crate::components::{Button, Identity};
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct TargetPickerProps {
pub targets: Box<[CharacterIdentity]>,
pub marked: Box<[CharacterId]>,
pub mark_callback: Option<Callback<CharacterId>>,
pub continue_callback: Option<Callback<()>>,
}
#[function_component]
pub fn TargetPicker(
TargetPickerProps {
targets,
marked,
mark_callback,
continue_callback,
}: &TargetPickerProps,
) -> Html {
let targets = targets
.iter()
.map(|t| {
let cb = mark_callback.clone();
let marked = marked.contains(&t.character_id);
html! {
<TargetCard target={t.clone()} marked={marked} mark_callback={cb}/>
}
})
.collect::<Html>();
html! {
<div class="target-picker">
<div class="targets">
{targets}
</div>
<Button on_click={continue_callback.clone().unwrap_or_default()}>
{"continue"}
</Button>
</div>
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct TargetCardProps {
pub target: CharacterIdentity,
pub marked: bool,
pub mark_callback: Option<Callback<CharacterId>>,
}
#[function_component]
pub fn TargetCard(
TargetCardProps {
target,
marked,
mark_callback,
}: &TargetCardProps,
) -> Html {
let click_target = target.character_id.clone();
let on_click = mark_callback
.clone()
.map(|cb| Callback::from(move |_| cb.emit(click_target.clone())))
.unwrap_or_default();
let marked = marked.then_some("marked");
let ident: PublicIdentity = target.into();
html! {
<Button on_click={on_click} classes={classes!(marked, "character")}>
<Identity ident={ident}/>
</Button>
}
}

View File

@ -13,7 +13,7 @@ use yew::prelude::*;
use crate::components::{
Button, CoverOfDarkness, Identity,
action::{BinaryChoice, OptionalSingleTarget, SingleTarget, TwoTarget, WolvesIntro},
action::{BinaryChoice, TargetPicker, WolvesIntro},
};
#[derive(Debug, Clone, PartialEq, Properties)]
@ -41,241 +41,62 @@ fn identity_html(props: &ActionPromptProps, ident: Option<&CharacterIdentity>) -
#[function_component]
pub fn Prompt(props: &ActionPromptProps) -> Html {
match &props.prompt {
let on_complete = props.on_complete.clone();
let continue_callback = props.big_screen.not().then(|| {
Callback::from(move |_| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Continue),
)))
})
});
let on_complete = props.on_complete.clone();
let mark_callback = props.big_screen.not().then(|| {
Callback::from(move |target: CharacterId| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::MarkTarget(target)),
)));
})
});
let (character_id, targets, marked, role_info) = match &props.prompt {
ActionPrompt::CoverOfDarkness => {
let on_complete = props.on_complete.clone();
let next = props.big_screen.not().then(|| {
Callback::from(move |_| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::ClearCoverOfDarkness),
)))
})
});
return html! {
<CoverOfDarkness next={next} />
<CoverOfDarkness next={continue_callback}/>
};
}
ActionPrompt::WolvesIntro { wolves } => {
let on_complete = props.on_complete.clone();
let on_complete = Callback::from(move |_| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(
werewolves_proto::message::night::ActionResponse::WolvesIntroAck,
),
)))
});
html! {
return html! {
<WolvesIntro
big_screen={props.big_screen}
on_complete={on_complete}
on_complete={continue_callback}
wolves={wolves.clone()}
/>
}
}
ActionPrompt::Seer {
character_id,
living_players,
} => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: CharacterId| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Seer(target)),
)));
})
});
html! {
<div>
{identity_html(props, Some(&character_id))}
<SingleTarget
targets={living_players.clone()}
target_selection={on_select}
headline={"check alignment"}
/>
</div>
}
};
}
ActionPrompt::RoleChange {
character_id,
new_role,
} => {
let on_complete = props.on_complete.clone();
let on_click = Callback::from(move |_| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::RoleChangeAck),
)))
});
let cont = props.big_screen.not().then(|| {
let cont = continue_callback.map(|continue_callback| {
html! {
<Button on_click={on_click}>{"continue"}</Button>
<Button on_click={continue_callback}>
{"continue"}
</Button>
}
});
html! {
<div>
{identity_html(props, Some(&character_id))}
return html! {
<div class="role-change">
{identity_html(props, Some(character_id))}
<h2>{"your role has changed"}</h2>
<p>{new_role.to_string()}</p>
<h1>{new_role.to_string()}</h1>
{cont}
</div>
}
}
ActionPrompt::Protector {
character_id,
targets,
} => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: CharacterId| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Protector(target)),
)));
})
});
html! {
<div>
{identity_html(props, Some(&character_id))}
<SingleTarget
targets={targets.clone()}
target_selection={on_select}
headline={"protector"}
/>
</div>
}
}
ActionPrompt::Arcanist {
character_id,
living_players,
} => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
Callback::from(move |(t1, t2): (CharacterId, CharacterId)| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Arcanist(t1, t2)),
)));
})
});
html! {
<div>
{identity_html(props, Some(&character_id))}
<TwoTarget
targets={living_players.clone()}
target_selection={on_select}
headline={"arcanist"}
/>
</div>
}
}
ActionPrompt::Gravedigger {
character_id,
dead_players,
} => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: CharacterId| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Gravedigger(target)),
)));
})
});
html! {
<div>
{identity_html(props, Some(&character_id))}
<SingleTarget
targets={dead_players.clone()}
target_selection={on_select}
headline={"gravedigger"}
/>
</div>
}
}
ActionPrompt::Hunter {
character_id,
current_target,
living_players,
} => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: CharacterId| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Hunter(target)),
)));
})
});
html! {
<div>
{identity_html(props, Some(&character_id))}
<SingleTarget
targets={living_players.clone()}
target_selection={on_select}
headline={"hunter"}
>
<h3>
<b>{"current target: "}</b>{current_target.clone().map(|t| html!{
<Identity ident={Into::<PublicIdentity>::into(t)} />
}).unwrap_or_else(|| html!{<i>{"none"}</i>})}
</h3>
</SingleTarget>
</div>
}
}
ActionPrompt::Militia {
character_id,
living_players,
} => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: Option<CharacterId>| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Militia(target)),
)));
})
});
html! {
<div>
{identity_html(props, Some(&character_id))}
<OptionalSingleTarget
targets={living_players.clone()}
target_selection={on_select}
headline={"pew pew?"}
/>
</div>
}
}
ActionPrompt::MapleWolf {
character_id,
kill_or_die,
living_players,
} => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: Option<CharacterId>| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::MapleWolf(target)),
)));
})
});
let kill_or_die = kill_or_die.then(|| {
html! {
<em>{"if you fail to eat tonight, you will starve"}</em>
}
});
html! {
<div>
{identity_html(props, Some(&character_id))}
<OptionalSingleTarget
targets={living_players.clone()}
target_selection={on_select}
headline={"nom nom?"}
>
{kill_or_die}
</OptionalSingleTarget>
</div>
}
};
}
ActionPrompt::Guardian {
character_id,
previous,
living_players,
marked,
} => {
let last_protect = previous.as_ref().map(|prev| match prev {
PreviousGuardianAction::Protect(target) => {
@ -293,108 +114,515 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
</>
},
});
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then_some({
move |prot| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Guardian(prot)),
)));
}
});
let marked = marked.iter().cloned().collect::<Box<[CharacterId]>>();
html! {
return html! {
<div>
{identity_html(props, Some(&character_id))}
<SingleTarget
targets={living_players.clone()}
target_selection={on_select}
headline={"pick someone to protect"}
>
{last_protect}
</SingleTarget>
{identity_html(props, Some(character_id))}
<h2>{"guardian"}</h2>
{last_protect}
<TargetPicker
targets={living_players.clone()}
marked={marked}
mark_callback={mark_callback}
continue_callback={continue_callback}
/>
</div>
}
}
ActionPrompt::WolfPackKill { living_villagers } => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: CharacterId| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::WolfPackKillVote(target)),
)));
})
});
html! {
<SingleTarget
targets={living_villagers.clone()}
target_selection={on_select}
headline={"wolf pack kill"}
/>
}
};
}
ActionPrompt::Seer {
character_id,
living_players,
marked,
} => (
Some(character_id),
living_players,
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
html! {{"seer"}},
),
ActionPrompt::Protector {
character_id,
targets,
marked,
} => (
Some(character_id),
targets,
marked.iter().cloned().collect(),
html! {{"protector"}},
),
ActionPrompt::Arcanist {
character_id,
living_players,
marked,
} => (
Some(character_id),
living_players,
[&marked.0, &marked.1]
.iter()
.filter_map(|c| (*c).clone())
.collect(),
html! {{"arcanist"}},
),
ActionPrompt::Gravedigger {
character_id,
dead_players,
marked,
} => (
Some(character_id),
dead_players,
marked.iter().cloned().collect(),
html! {{"gravedigger"}},
),
ActionPrompt::Hunter {
character_id,
current_target,
living_players,
marked,
} => (
Some(character_id),
living_players,
marked.iter().cloned().collect(),
{
let current_target = current_target.as_ref().cloned().map(|t| {
html! {
<>
<h3>{"current target:"}</h3>
<Identity ident={Into::<PublicIdentity>::into(t)} />
</>
}
});
html! {
<>
<h2>{"hunter"}</h2>
{current_target}
</>
}
},
),
ActionPrompt::Militia {
character_id,
living_players,
marked,
} => (
Some(character_id),
living_players,
marked.iter().cloned().collect(),
html! {{"militia"}},
),
ActionPrompt::MapleWolf {
character_id,
kill_or_die,
living_players,
marked,
} => (
Some(character_id),
living_players,
marked.iter().cloned().collect(),
html! {<>{"maple wolf"} {kill_or_die.then_some(" — starving")}</>},
),
ActionPrompt::WolfPackKill {
living_villagers,
marked,
} => (
None,
living_villagers,
marked.iter().cloned().collect(),
html! {{"wolfpack kill"}},
),
ActionPrompt::Shapeshifter { character_id } => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then_some({
move |shift| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Shapeshifter(shift)),
HostNightMessage::ActionResponse(if shift {
ActionResponse::Shapeshift
} else {
ActionResponse::Continue
}),
)));
}
});
html! {
return html! {
<div>
{identity_html(props, Some(&character_id))}
{identity_html(props, Some(character_id))}
<BinaryChoice on_chosen={on_select}>
<h2>{"shapeshift?"}</h2>
</BinaryChoice>
</div>
}
};
}
ActionPrompt::AlphaWolf {
character_id,
living_villagers,
} => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: Option<CharacterId>| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::AlphaWolf(target)),
)));
})
});
html! {
<div>
{identity_html(props, Some(&character_id))}
<OptionalSingleTarget
targets={living_villagers.clone()}
target_selection={on_select}
headline={"alpha wolf target"}
/>
</div>
}
}
marked,
} => (
Some(character_id),
living_villagers,
marked.iter().cloned().collect(),
html! {{"alpha wolf"}},
),
ActionPrompt::DireWolf {
character_id,
living_players,
} => {
let on_complete = props.on_complete.clone();
let on_select = props.big_screen.not().then(|| {
Callback::from(move |target: CharacterId| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Direwolf(target)),
)));
})
});
html! {
<div>
{identity_html(props, Some(&character_id))}
<SingleTarget
targets={living_players.clone()}
target_selection={on_select}
headline={"direwolf block target"}
/>
</div>
}
}
marked,
} => (
Some(character_id),
living_players,
marked.iter().cloned().collect(),
html! {{"dire wolf"}},
),
};
html! {
<div class="prompt">
{identity_html(props, character_id)}
<h2>{role_info}</h2>
<TargetPicker
targets={targets.clone()}
marked={marked}
mark_callback={mark_callback}
continue_callback={continue_callback}
/>
</div>
}
// match &props.prompt {
// ActionPrompt::CoverOfDarkness => {
// let on_complete = props.on_complete.clone();
// let next = props.big_screen.not().then(|| {
// Callback::from(move |_| {
// on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
// HostNightMessage::ActionResponse(ActionResponse::ClearCoverOfDarkness),
// )))
// })
// });
// return html! {
// <CoverOfDarkness next={next} />
// };
// }
// ActionPrompt::WolvesIntro { wolves } => {
// let on_complete = props.on_complete.clone();
// let on_complete = Callback::from(move |_| {
// on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
// HostNightMessage::ActionResponse(
// werewolves_proto::message::night::ActionResponse::WolvesIntroAck,
// ),
// )))
// });
// html! {
// <WolvesIntro
// big_screen={props.big_screen}
// on_complete={on_complete}
// wolves={wolves.clone()}
// />
// }
// }
// ActionPrompt::Seer {
// character_id,
// living_players,
// } => {
// let on_complete = props.on_complete.clone();
// let on_select = props.big_screen.not().then(|| {
// Callback::from(move |target: CharacterId| {
// on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
// HostNightMessage::ActionResponse(ActionResponse::Seer(target)),
// )));
// })
// });
// html! {
// <div>
// {identity_html(props, Some(&character_id))}
// <SingleTarget
// targets={living_players.clone()}
// target_selection={on_select}
// headline={"check alignment"}
// />
// </div>
// }
// }
// ActionPrompt::RoleChange {
// character_id,
// new_role,
// } => {
// let on_complete = props.on_complete.clone();
// let on_click = Callback::from(move |_| {
// on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
// HostNightMessage::ActionResponse(ActionResponse::RoleChangeAck),
// )))
// });
// let cont = props.big_screen.not().then(|| {
// html! {
// <Button on_click={on_click}>{"continue"}</Button>
// }
// });
// html! {
// <div>
// {identity_html(props, Some(&character_id))}
// <h2>{"your role has changed"}</h2>
// <p>{new_role.to_string()}</p>
// {cont}
// </div>
// }
// }
// ActionPrompt::Protector {
// character_id,
// targets,
// } => {
// let on_complete = props.on_complete.clone();
// let on_select = props.big_screen.not().then(|| {
// Callback::from(move |target: CharacterId| {
// on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
// HostNightMessage::ActionResponse(ActionResponse::Protector(target)),
// )));
// })
// });
// html! {
// <div>
// {identity_html(props, Some(&character_id))}
// <SingleTarget
// targets={targets.clone()}
// target_selection={on_select}
// headline={"protector"}
// />
// </div>
// }
// }
// ActionPrompt::Arcanist {
// character_id,
// living_players,
// } => {
// let on_complete = props.on_complete.clone();
// let on_select = props.big_screen.not().then(|| {
// Callback::from(move |(t1, t2): (CharacterId, CharacterId)| {
// on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
// HostNightMessage::ActionResponse(ActionResponse::Arcanist(t1, t2)),
// )));
// })
// });
// html! {
// <div>
// {identity_html(props, Some(&character_id))}
// <TwoTarget
// targets={living_players.clone()}
// target_selection={on_select}
// headline={"arcanist"}
// />
// </div>
// }
// }
// ActionPrompt::Gravedigger {
// character_id,
// dead_players,
// } => {
// let on_complete = props.on_complete.clone();
// let on_select = props.big_screen.not().then(|| {
// Callback::from(move |target: CharacterId| {
// on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
// HostNightMessage::ActionResponse(ActionResponse::Gravedigger(target)),
// )));
// })
// });
// html! {
// <div>
// {identity_html(props, Some(&character_id))}
// <SingleTarget
// targets={dead_players.clone()}
// target_selection={on_select}
// headline={"gravedigger"}
// />
// </div>
// }
// }
// ActionPrompt::Hunter {
// character_id,
// current_target,
// living_players,
// } => {
// let on_complete = props.on_complete.clone();
// let on_select = props.big_screen.not().then(|| {
// Callback::from(move |target: CharacterId| {
// on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
// HostNightMessage::ActionResponse(ActionResponse::Hunter(target)),
// )));
// })
// });
// html! {
// <div>
// {identity_html(props, Some(&character_id))}
// <SingleTarget
// targets={living_players.clone()}
// target_selection={on_select}
// headline={"hunter"}
// >
// <h3>
// <b>{"current target: "}</b>{current_target.clone().map(|t| html!{
// <Identity ident={Into::<PublicIdentity>::into(t)} />
// }).unwrap_or_else(|| html!{<i>{"none"}</i>})}
// </h3>
// </SingleTarget>
// </div>
// }
// }
// ActionPrompt::Militia {
// character_id,
// living_players,
// } => {
// let on_complete = props.on_complete.clone();
// let on_select = props.big_screen.not().then(|| {
// Callback::from(move |target: Option<CharacterId>| {
// on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
// HostNightMessage::ActionResponse(ActionResponse::Militia(target)),
// )));
// })
// });
// html! {
// <div>
// {identity_html(props, Some(&character_id))}
// <OptionalSingleTarget
// targets={living_players.clone()}
// target_selection={on_select}
// headline={"pew pew?"}
// />
// </div>
// }
// }
// ActionPrompt::MapleWolf {
// character_id,
// kill_or_die,
// living_players,
// marked,
// } => {
// let kill_or_die = kill_or_die.then(|| {
// html! {
// <em>{"if you fail to eat tonight, you will starve"}</em>
// }
// });
// html! {
// <div>
// {identity_html(props, Some(&character_id))}
// <OptionalSingleTarget
// targets={living_players.clone()}
// target_selection={on_select}
// headline={"nom nom?"}
// >
// {kill_or_die}
// </OptionalSingleTarget>
// </div>
// }
// }
// ActionPrompt::Guardian {
// character_id,
// previous,
// living_players,
// marked,
// } => {
// let last_protect = previous.as_ref().map(|prev| match prev {
// PreviousGuardianAction::Protect(target) => {
// html! {
// <>
// <b>{"last night you protected: "}</b>
// <Identity ident={Into::<PublicIdentity>::into(target)}/>
// </>
// }
// }
// PreviousGuardianAction::Guard(target) => html! {
// <>
// <b>{"last night you guarded: "}</b>
// <Identity ident={Into::<PublicIdentity>::into(target)}/>
// </>
// },
// });
// let marked = marked.iter().cloned().collect();
// html! {
// <div>
// {identity_html(props, Some(&character_id))}
// <h2>{"guardian"}</h2>
// {last_protect}
// <TargetPicker
// targets={living_players.clone()}
// marked={marked}
// mark_callback={mark_callback}
// continue_callback={continue_callback}
// />
// </div>
// }
// }
// ActionPrompt::WolfPackKill { living_villagers } => {
// let on_complete = props.on_complete.clone();
// html! {
// <SingleTarget
// targets={living_villagers.clone()}
// target_selection={on_select}
// headline={"wolf pack kill"}
// />
// }
// }
// ActionPrompt::Shapeshifter { character_id } => {
// let on_complete = props.on_complete.clone();
// let on_select = props.big_screen.not().then_some({
// move |shift| {
// on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
// HostNightMessage::ActionResponse(ActionResponse::Shapeshifter(shift)),
// )));
// }
// });
// html! {
// <div>
// {identity_html(props, Some(&character_id))}
// <BinaryChoice on_chosen={on_select}>
// <h2>{"shapeshift?"}</h2>
// </BinaryChoice>
// </div>
// }
// }
// ActionPrompt::AlphaWolf {
// character_id,
// living_villagers,
// } => {
// let on_complete = props.on_complete.clone();
// let on_select = props.big_screen.not().then(|| {
// Callback::from(move |target: Option<CharacterId>| {
// on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
// HostNightMessage::ActionResponse(ActionResponse::AlphaWolf(target)),
// )));
// })
// });
// html! {
// <div>
// {identity_html(props, Some(&character_id))}
// <OptionalSingleTarget
// targets={living_villagers.clone()}
// target_selection={on_select}
// headline={"alpha wolf target"}
// />
// </div>
// }
// }
// ActionPrompt::DireWolf {
// character_id,
// living_players,
// } => {
// let on_complete = props.on_complete.clone();
// let on_select = props.big_screen.not().then(|| {
// Callback::from(move |target: CharacterId| {
// on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
// HostNightMessage::ActionResponse(ActionResponse::Direwolf(target)),
// )));
// })
// });
// html! {
// <div>
// {identity_html(props, Some(&character_id))}
// <SingleTarget
// targets={living_players.clone()}
// target_selection={on_select}
// headline={"direwolf block target"}
// />
// </div>
// }
// }
// }
}

View File

@ -11,14 +11,13 @@ use werewolves_proto::{
};
use yew::prelude::*;
use crate::components::{CoverOfDarkness, Identity};
use crate::components::{Button, CoverOfDarkness, Identity};
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct ActionResultProps {
pub result: ActionResult,
#[prop_or_default]
pub ident: Option<PublicIdentity>,
#[prop_or_default]
pub big_screen: bool,
pub on_complete: Callback<HostMessage>,
}
@ -40,7 +39,7 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
let cont = props
.big_screen
.not()
.then(|| html! {<button onclick={on_complete}>{"continue"}</button>});
.then(|| html! {<Button on_click={on_complete}>{"continue"}</Button>});
match &props.result {
ActionResult::RoleBlocked => {
html! {

View File

@ -1,5 +1,3 @@
use core::ops::Not;
use werewolves_proto::{
message::{CharacterIdentity, PublicIdentity},
role::RoleTitle,
@ -11,14 +9,18 @@ use crate::components::{Button, Identity};
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct WolvesIntroProps {
pub wolves: Box<[(CharacterIdentity, RoleTitle)]>,
pub big_screen: bool,
pub on_complete: Callback<()>,
pub on_complete: Option<Callback<()>>,
}
#[function_component]
pub fn WolvesIntro(props: &WolvesIntroProps) -> Html {
let on_complete = props.on_complete.clone();
let on_complete = Callback::from(move |_| on_complete.emit(()));
let on_complete = props.on_complete.clone().map(|on_complete| {
html! {
<Button on_click={Callback::from(move |_| on_complete.emit(()))}>
{"continue"}
</Button>
}
});
html! {
<div class="wolves-intro">
<h2>{"these are the wolves:"}</h2>
@ -32,11 +34,7 @@ pub fn WolvesIntro(props: &WolvesIntroProps) -> Html {
}).collect::<Html>()
}
</div>
{
props.big_screen.not().then_some(html!{
<Button on_click={on_complete}>{"continue"}</Button>
})
}
{on_complete}
</div>
}
}

View File

@ -91,9 +91,14 @@ pub fn DaytimePlayer(
let dead = died_to.is_some().then_some("dead");
let marked = on_the_block.then_some("marked");
let character_id = identity.character_id.clone();
let on_click: Callback<_> = on_select
.clone()
.map(|on_select| Callback::from(move |_| on_select.emit(character_id.clone())))
let on_click: Callback<_> = died_to
.is_none()
.then_some(())
.and(
on_select
.clone()
.map(|on_select| Callback::from(move |_| on_select.emit(character_id.clone()))),
)
.unwrap_or_default();
let identity: PublicIdentity = identity.into();
html! {