target split, more prompts, improved kills
This commit is contained in:
parent
e704fdca8b
commit
d352cfb1ee
|
|
@ -1,14 +1,16 @@
|
|||
use core::{fmt::Debug, num::NonZeroU8};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use werewolves_macros::Extract;
|
||||
|
||||
use crate::{game::DateTime, player::CharacterId};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Extract)]
|
||||
pub enum DiedTo {
|
||||
Execution {
|
||||
day: NonZeroU8,
|
||||
},
|
||||
#[extract(source as killer)]
|
||||
MapleWolf {
|
||||
source: CharacterId,
|
||||
night: NonZeroU8,
|
||||
|
|
@ -17,13 +19,17 @@ pub enum DiedTo {
|
|||
MapleWolfStarved {
|
||||
night: NonZeroU8,
|
||||
},
|
||||
#[extract(killer as killer)]
|
||||
Militia {
|
||||
killer: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
#[extract(killing_wolf as killer)]
|
||||
Wolfpack {
|
||||
killing_wolf: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
#[extract(killer as killer)]
|
||||
AlphaWolf {
|
||||
killer: CharacterId,
|
||||
night: NonZeroU8,
|
||||
|
|
@ -32,12 +38,17 @@ pub enum DiedTo {
|
|||
into: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
#[extract(killer as killer)]
|
||||
Hunter {
|
||||
killer: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
Guardian {
|
||||
killer: CharacterId,
|
||||
#[extract(source as killer)]
|
||||
GuardianProtecting {
|
||||
source: CharacterId,
|
||||
protecting: CharacterId,
|
||||
protecting_from: CharacterId,
|
||||
protecting_from_cause: Box<DiedTo>,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
}
|
||||
|
|
@ -47,7 +58,13 @@ impl DiedTo {
|
|||
match self {
|
||||
DiedTo::Execution { day } => DateTime::Day { number: *day },
|
||||
|
||||
DiedTo::Guardian { killer: _, night }
|
||||
DiedTo::GuardianProtecting {
|
||||
source: _,
|
||||
protecting: _,
|
||||
protecting_from: _,
|
||||
protecting_from_cause: _,
|
||||
night,
|
||||
}
|
||||
| DiedTo::MapleWolf {
|
||||
source: _,
|
||||
night,
|
||||
|
|
@ -55,7 +72,10 @@ impl DiedTo {
|
|||
}
|
||||
| DiedTo::MapleWolfStarved { night }
|
||||
| DiedTo::Militia { killer: _, night }
|
||||
| DiedTo::Wolfpack { night }
|
||||
| DiedTo::Wolfpack {
|
||||
night,
|
||||
killing_wolf: _,
|
||||
}
|
||||
| DiedTo::AlphaWolf { killer: _, night }
|
||||
| DiedTo::Shapeshift { into: _, night }
|
||||
| DiedTo::Hunter { killer: _, night } => DateTime::Night {
|
||||
|
|
|
|||
|
|
@ -69,4 +69,8 @@ pub enum GameError {
|
|||
GameOngoing,
|
||||
#[error("needs a role reveal")]
|
||||
NeedRoleReveal,
|
||||
#[error("no previous state")]
|
||||
NoPreviousState,
|
||||
#[error("invalid original kill for guardian guard")]
|
||||
GuardianInvalidOriginalKill,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,292 @@
|
|||
use core::{
|
||||
num::NonZeroU8,
|
||||
ops::{Deref, Not},
|
||||
};
|
||||
|
||||
use super::Result;
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{Village, kill::taken::Taken, night::NightChange},
|
||||
player::{CharacterId, Protection},
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum KillOutcome {
|
||||
Single(CharacterId, DiedTo),
|
||||
Guarding {
|
||||
original_killer: CharacterId,
|
||||
original_target: CharacterId,
|
||||
original_kill: DiedTo,
|
||||
guardian: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
}
|
||||
|
||||
impl KillOutcome {
|
||||
pub fn apply_to_village(self, village: &mut Village) -> Result<()> {
|
||||
match self {
|
||||
KillOutcome::Single(character_id, died_to) => Ok(village
|
||||
.character_by_id_mut(&character_id)
|
||||
.ok_or(GameError::InvalidTarget)?
|
||||
.kill(died_to)),
|
||||
KillOutcome::Guarding {
|
||||
original_killer,
|
||||
original_target,
|
||||
original_kill,
|
||||
guardian,
|
||||
night,
|
||||
} => {
|
||||
// check if guardian exists before we mutably borrow killer, which would
|
||||
// prevent us from borrowing village to check after.
|
||||
village
|
||||
.character_by_id(&guardian)
|
||||
.ok_or(GameError::InvalidTarget)?;
|
||||
village
|
||||
.character_by_id_mut(&original_killer)
|
||||
.ok_or(GameError::InvalidTarget)?
|
||||
.kill(DiedTo::GuardianProtecting {
|
||||
night,
|
||||
source: guardian.clone(),
|
||||
protecting: original_target,
|
||||
protecting_from: original_killer,
|
||||
protecting_from_cause: Box::new(original_kill.clone()),
|
||||
});
|
||||
village
|
||||
.character_by_id_mut(&guardian)
|
||||
.ok_or(GameError::InvalidTarget)?
|
||||
.kill(original_kill);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_protection(
|
||||
killer: CharacterId,
|
||||
killed_with: &DiedTo,
|
||||
target: &CharacterId,
|
||||
protection: &Protection,
|
||||
night: NonZeroU8,
|
||||
) -> Option<KillOutcome> {
|
||||
match protection {
|
||||
Protection::Guardian {
|
||||
source,
|
||||
guarding: true,
|
||||
} => Some(KillOutcome::Guarding {
|
||||
original_killer: killer,
|
||||
guardian: source.clone(),
|
||||
original_target: target.clone(),
|
||||
original_kill: killed_with.clone(),
|
||||
night,
|
||||
}),
|
||||
Protection::Guardian {
|
||||
source: _,
|
||||
guarding: false,
|
||||
}
|
||||
| Protection::Protector { source: _ } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_kill(
|
||||
changes: &mut ChangesLookup<'_>,
|
||||
target: &CharacterId,
|
||||
died_to: &DiedTo,
|
||||
night: u8,
|
||||
village: &Village,
|
||||
) -> Result<Option<KillOutcome>> {
|
||||
if let DiedTo::MapleWolf {
|
||||
source,
|
||||
night,
|
||||
starves_if_fails: true,
|
||||
} = died_to
|
||||
&& let Some(protection) = changes.protected_take(target)
|
||||
{
|
||||
return Ok(Some(
|
||||
resolve_protection(source.clone(), died_to, target, &protection, *night).unwrap_or(
|
||||
KillOutcome::Single(source.clone(), DiedTo::MapleWolfStarved { night: *night }),
|
||||
),
|
||||
));
|
||||
}
|
||||
if let DiedTo::Wolfpack {
|
||||
night,
|
||||
killing_wolf,
|
||||
} = died_to
|
||||
&& let Some(ss_source) = changes.shapeshifter()
|
||||
{
|
||||
let killing_wolf = village
|
||||
.character_by_id(killing_wolf)
|
||||
.ok_or(GameError::InvalidTarget)?;
|
||||
|
||||
match changes.protected_take(target) {
|
||||
Some(protection) => {
|
||||
return Ok(resolve_protection(
|
||||
killing_wolf.character_id().clone(),
|
||||
died_to,
|
||||
target,
|
||||
&protection,
|
||||
*night,
|
||||
));
|
||||
}
|
||||
None => {
|
||||
// Wolf kill went through -- can kill shifter
|
||||
return Ok(Some(KillOutcome::Single(
|
||||
ss_source.clone(),
|
||||
DiedTo::Shapeshift {
|
||||
into: target.clone(),
|
||||
night: *night,
|
||||
},
|
||||
)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let protection = match changes.protected_take(target) {
|
||||
Some(prot) => prot,
|
||||
None => return Ok(Some(KillOutcome::Single(target.clone(), died_to.clone()))),
|
||||
};
|
||||
|
||||
match protection.deref() {
|
||||
Protection::Guardian {
|
||||
source,
|
||||
guarding: true,
|
||||
} => Ok(Some(KillOutcome::Guarding {
|
||||
original_killer: died_to
|
||||
.killer()
|
||||
.ok_or(GameError::GuardianInvalidOriginalKill)?
|
||||
.clone(),
|
||||
original_target: target.clone(),
|
||||
original_kill: died_to.clone(),
|
||||
guardian: source.clone(),
|
||||
night: NonZeroU8::new(night).unwrap(),
|
||||
})),
|
||||
Protection::Guardian {
|
||||
source: _,
|
||||
guarding: false,
|
||||
}
|
||||
| Protection::Protector { source: _ } => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChangesLookup<'a>(&'a [NightChange], Vec<usize>);
|
||||
|
||||
impl<'a> ChangesLookup<'a> {
|
||||
pub fn new(changes: &'a [NightChange]) -> Self {
|
||||
Self(changes, Vec::new())
|
||||
}
|
||||
|
||||
pub fn killed(&self, target: &CharacterId) -> Option<&'a DiedTo> {
|
||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||
self.1
|
||||
.contains(&idx)
|
||||
.not()
|
||||
.then(|| match c {
|
||||
NightChange::Kill { target: t, died_to } => (t == target).then_some(died_to),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn release<T>(&mut self, taken: Taken<'a, T>) {
|
||||
self.1.swap_remove(
|
||||
self.1
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(idx, c)| (*c == taken.idx()).then_some(idx))
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn protected_take(&mut self, target: &CharacterId) -> Option<Taken<'a, Protection>> {
|
||||
if let Some((idx, c)) = self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||
self.1
|
||||
.contains(&idx)
|
||||
.not()
|
||||
.then(|| match c {
|
||||
NightChange::Protection {
|
||||
target: t,
|
||||
protection,
|
||||
} => (t == target).then_some((idx, protection)),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
}) {
|
||||
self.1.push(idx);
|
||||
Some(Taken::new(idx, c))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> {
|
||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||
self.1
|
||||
.contains(&idx)
|
||||
.not()
|
||||
.then(|| match c {
|
||||
NightChange::Protection {
|
||||
target: t,
|
||||
protection,
|
||||
} => (t == target).then_some(protection),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn shapeshifter(&self) -> Option<&'a CharacterId> {
|
||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||
self.1
|
||||
.contains(&idx)
|
||||
.not()
|
||||
.then_some(match c {
|
||||
NightChange::Shapeshift { source } => Some(source),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn wolf_pack_kill_target(&self) -> Option<&'a CharacterId> {
|
||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||
self.1
|
||||
.contains(&idx)
|
||||
.not()
|
||||
.then_some(match c {
|
||||
NightChange::Kill {
|
||||
target,
|
||||
died_to:
|
||||
DiedTo::Wolfpack {
|
||||
night: _,
|
||||
killing_wolf: _,
|
||||
},
|
||||
} => Some(target),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
mod taken {
|
||||
use core::ops::Deref;
|
||||
|
||||
pub struct Taken<'a, T>(usize, &'a T);
|
||||
impl<'a, T> Taken<'a, T> {
|
||||
pub const fn new(idx: usize, item: &'a T) -> Self {
|
||||
Self(idx, item)
|
||||
}
|
||||
|
||||
pub const fn idx(&self) -> usize {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Deref for Taken<'a, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
mod kill;
|
||||
mod night;
|
||||
mod settings;
|
||||
mod village;
|
||||
|
|
@ -27,12 +28,14 @@ type Result<T> = core::result::Result<T, GameError>;
|
|||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Game {
|
||||
previous: Vec<GameState>,
|
||||
next: Vec<GameState>,
|
||||
state: GameState,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
|
||||
Ok(Self {
|
||||
next: Vec::new(),
|
||||
previous: Vec::new(),
|
||||
state: GameState::Night {
|
||||
night: Night::new(Village::new(players, settings)?)?,
|
||||
|
|
@ -161,6 +164,24 @@ impl Game {
|
|||
},
|
||||
HostGameMessage::Night(_),
|
||||
) => Err(GameError::InvalidMessageForGameState),
|
||||
(
|
||||
GameState::Day {
|
||||
village: _,
|
||||
marked: _,
|
||||
},
|
||||
HostGameMessage::PreviousState,
|
||||
) => {
|
||||
let mut prev = self.previous.pop().ok_or(GameError::NoPreviousState)?;
|
||||
log::info!("previous state loaded: {prev:?}");
|
||||
core::mem::swap(&mut prev, &mut self.state);
|
||||
self.next.push(prev);
|
||||
|
||||
self.process(HostGameMessage::GetState)
|
||||
}
|
||||
(GameState::Night { night }, HostGameMessage::PreviousState) => {
|
||||
night.previous_state()?;
|
||||
self.process(HostGameMessage::GetState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ use super::Result;
|
|||
use crate::{
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{DateTime, Village},
|
||||
game::{
|
||||
DateTime, Village,
|
||||
kill::{self, ChangesLookup},
|
||||
},
|
||||
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
||||
player::{Character, CharacterId, Protection},
|
||||
role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle},
|
||||
|
|
@ -19,6 +22,7 @@ pub struct Night {
|
|||
village: Village,
|
||||
night: u8,
|
||||
action_queue: VecDeque<(ActionPrompt, Character)>,
|
||||
used_actions: Vec<(ActionPrompt, Character)>,
|
||||
changes: Vec<NightChange>,
|
||||
night_state: NightState,
|
||||
}
|
||||
|
|
@ -166,15 +170,62 @@ impl Night {
|
|||
village,
|
||||
night_state,
|
||||
action_queue,
|
||||
used_actions: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn previous_state(&mut self) -> Result<()> {
|
||||
let (prev_act, prev_char) = self.used_actions.pop().ok_or(GameError::NoPreviousState)?;
|
||||
log::info!("loading previous prompt: {prev_act:?}");
|
||||
match &self.night_state {
|
||||
NightState::Active {
|
||||
current_prompt,
|
||||
current_char,
|
||||
current_result: Some(current_result),
|
||||
} => {
|
||||
// if let Some(last_change) = self.changes.pop() {
|
||||
|
||||
// }
|
||||
log::info!("removing current result: {current_result:?}");
|
||||
self.night_state = NightState::Active {
|
||||
current_prompt: current_prompt.clone(),
|
||||
current_char: current_char.clone(),
|
||||
current_result: None,
|
||||
};
|
||||
}
|
||||
NightState::Active {
|
||||
current_prompt,
|
||||
current_char,
|
||||
current_result: None,
|
||||
} => {
|
||||
log::info!("pushing current prompt to front of action queue: {current_prompt:?}");
|
||||
self.action_queue.push_front((
|
||||
current_prompt.clone(),
|
||||
self.village.character_by_id(current_char).unwrap().clone(),
|
||||
));
|
||||
self.night_state = NightState::Active {
|
||||
current_prompt: prev_act,
|
||||
current_char: prev_char.character_id().clone(),
|
||||
current_result: None,
|
||||
}
|
||||
}
|
||||
NightState::Complete => {
|
||||
self.night_state = NightState::Active {
|
||||
current_prompt: prev_act,
|
||||
current_char: prev_char.character_id().clone(),
|
||||
current_result: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn collect_completed(&self) -> Result<Village> {
|
||||
if !matches!(self.night_state, NightState::Complete) {
|
||||
return Err(GameError::NotEndOfNight);
|
||||
}
|
||||
let mut new_village = self.village.clone();
|
||||
let changes = ChangesLookup(&self.changes);
|
||||
let mut changes = ChangesLookup::new(&self.changes);
|
||||
for change in self.changes.iter() {
|
||||
match change {
|
||||
NightChange::RoleChange(character_id, role_title) => new_village
|
||||
|
|
@ -201,108 +252,17 @@ impl Night {
|
|||
}
|
||||
}
|
||||
NightChange::Kill { target, died_to } => {
|
||||
if let DiedTo::MapleWolf {
|
||||
source,
|
||||
night,
|
||||
starves_if_fails: true,
|
||||
} = died_to
|
||||
&& changes.protected(target).is_some()
|
||||
{
|
||||
// kill maple first, then act as if they get their kill attempt
|
||||
new_village
|
||||
.character_by_id_mut(source)
|
||||
.unwrap()
|
||||
.kill(DiedTo::MapleWolfStarved { night: *night });
|
||||
if let Some(kill) = kill::resolve_kill(
|
||||
&mut changes,
|
||||
target,
|
||||
died_to,
|
||||
self.night,
|
||||
&self.village,
|
||||
)? {
|
||||
kill.apply_to_village(&mut new_village)?;
|
||||
}
|
||||
|
||||
if let Some(prot) = changes.protected(target) {
|
||||
match prot {
|
||||
Protection::Guardian {
|
||||
source,
|
||||
guarding: true,
|
||||
} => {
|
||||
let kill_source = match died_to {
|
||||
DiedTo::MapleWolfStarved { night } => {
|
||||
new_village
|
||||
.character_by_id_mut(target)
|
||||
.unwrap()
|
||||
.kill(DiedTo::MapleWolfStarved { night: *night });
|
||||
continue;
|
||||
}
|
||||
DiedTo::Execution { day: _ } => unreachable!(),
|
||||
DiedTo::MapleWolf {
|
||||
source,
|
||||
night: _,
|
||||
starves_if_fails: _,
|
||||
}
|
||||
| DiedTo::Militia {
|
||||
killer: source,
|
||||
night: _,
|
||||
}
|
||||
| DiedTo::AlphaWolf {
|
||||
killer: source,
|
||||
night: _,
|
||||
}
|
||||
| DiedTo::Hunter {
|
||||
killer: source,
|
||||
night: _,
|
||||
} => source.clone(),
|
||||
DiedTo::Wolfpack { night: _ } => {
|
||||
if let Some(wolf_to_kill) = new_village
|
||||
.living_wolf_pack_players()
|
||||
.into_iter()
|
||||
.find(|w| matches!(w.role(), Role::Werewolf))
|
||||
.map(|w| w.character_id().clone())
|
||||
.or_else(|| {
|
||||
new_village
|
||||
.living_wolf_pack_players()
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|w| w.character_id().clone())
|
||||
})
|
||||
{
|
||||
wolf_to_kill
|
||||
} else {
|
||||
// No wolves? Game over?
|
||||
continue;
|
||||
}
|
||||
}
|
||||
DiedTo::Shapeshift { into: _, night: _ } => target.clone(),
|
||||
DiedTo::Guardian {
|
||||
killer: _,
|
||||
night: _,
|
||||
} => continue,
|
||||
};
|
||||
new_village.character_by_id_mut(&kill_source).unwrap().kill(
|
||||
DiedTo::Guardian {
|
||||
killer: source.clone(),
|
||||
night: NonZeroU8::new(self.night).unwrap(),
|
||||
},
|
||||
);
|
||||
new_village.character_by_id_mut(source).unwrap().kill(
|
||||
DiedTo::Wolfpack {
|
||||
night: NonZeroU8::new(self.night).unwrap(),
|
||||
},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Protection::Guardian {
|
||||
source: _,
|
||||
guarding: false,
|
||||
} => continue,
|
||||
Protection::Protector { source: _ } => continue,
|
||||
}
|
||||
}
|
||||
|
||||
new_village
|
||||
.character_by_id_mut(target)
|
||||
.unwrap()
|
||||
.kill(DiedTo::Wolfpack {
|
||||
night: NonZeroU8::new(self.night).unwrap(),
|
||||
});
|
||||
}
|
||||
NightChange::Shapeshift { source } => {
|
||||
// TODO: shapeshift should probably notify immediately after it happens
|
||||
if let Some(target) = changes.wolf_pack_kill_target()
|
||||
&& changes.protected(target).is_none()
|
||||
{
|
||||
|
|
@ -317,13 +277,15 @@ impl Night {
|
|||
into: target.clone(),
|
||||
night: NonZeroU8::new(self.night).unwrap(),
|
||||
});
|
||||
let target = new_village.find_by_character_id_mut(target).unwrap();
|
||||
target
|
||||
.role_change(
|
||||
RoleTitle::Werewolf,
|
||||
DateTime::Night { number: self.night },
|
||||
)
|
||||
.unwrap();
|
||||
// role change pushed in [apply_shapeshift]
|
||||
|
||||
// let target = new_village.find_by_character_id_mut(target).unwrap();
|
||||
// target
|
||||
// .role_change(
|
||||
// RoleTitle::Werewolf,
|
||||
// DateTime::Night { number: self.night },
|
||||
// )
|
||||
// .unwrap();
|
||||
}
|
||||
}
|
||||
NightChange::RoleBlock {
|
||||
|
|
@ -347,7 +309,11 @@ impl Night {
|
|||
if let Some(kill_target) = self.changes.iter().find_map(|c| match c {
|
||||
NightChange::Kill {
|
||||
target,
|
||||
died_to: DiedTo::Wolfpack { night: _ },
|
||||
died_to:
|
||||
DiedTo::Wolfpack {
|
||||
night: _,
|
||||
killing_wolf: _,
|
||||
},
|
||||
} => Some(target.clone()),
|
||||
_ => None,
|
||||
}) {
|
||||
|
|
@ -361,22 +327,42 @@ impl Night {
|
|||
// there is protection, so the kill doesn't happen -> no shapeshift
|
||||
return Ok(ActionResult::GoBackToSleep);
|
||||
}
|
||||
if let Some(kill) = self.changes.iter_mut().find(|c| {
|
||||
// if let Some(kill) = self.changes.iter_mut().find(|c| {
|
||||
// matches!(
|
||||
// c,
|
||||
// NightChange::Kill {
|
||||
// target: _,
|
||||
// died_to: DiedTo::Wolfpack { night: _ }
|
||||
// }
|
||||
// )
|
||||
// }) {
|
||||
// *kill = NightChange::Kill {
|
||||
// target: source.clone(),
|
||||
// died_to: DiedTo::Shapeshift {
|
||||
// into: kill_target.clone(),
|
||||
// night: NonZeroU8::new(self.night).unwrap(),
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
if self.changes.iter_mut().any(|c| {
|
||||
matches!(
|
||||
c,
|
||||
NightChange::Kill {
|
||||
target: _,
|
||||
died_to: DiedTo::Wolfpack { night: _ }
|
||||
died_to: DiedTo::Wolfpack {
|
||||
night: _,
|
||||
killing_wolf: _
|
||||
}
|
||||
}
|
||||
)
|
||||
}) {
|
||||
*kill = NightChange::Kill {
|
||||
self.changes.push(NightChange::Kill {
|
||||
target: source.clone(),
|
||||
died_to: DiedTo::Shapeshift {
|
||||
into: kill_target.clone(),
|
||||
night: NonZeroU8::new(self.night).unwrap(),
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
self.changes.push(NightChange::Shapeshift {
|
||||
source: source.clone(),
|
||||
|
|
@ -780,7 +766,10 @@ impl Night {
|
|||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::Kill {
|
||||
target: target.clone(),
|
||||
died_to: DiedTo::Wolfpack { night },
|
||||
died_to: DiedTo::Wolfpack {
|
||||
night,
|
||||
killing_wolf: self.village.killing_wolf_id(),
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(target)),
|
||||
})
|
||||
|
|
@ -980,6 +969,7 @@ impl Night {
|
|||
NightState::Complete => return Err(GameError::NightOver),
|
||||
}
|
||||
if let Some((prompt, character)) = self.action_queue.pop_front() {
|
||||
self.used_actions.push((prompt.clone(), character.clone()));
|
||||
self.night_state = NightState::Active {
|
||||
current_prompt: prompt,
|
||||
current_char: character.character_id().clone(),
|
||||
|
|
@ -996,34 +986,3 @@ impl Night {
|
|||
self.changes.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
struct ChangesLookup<'a>(&'a [NightChange]);
|
||||
|
||||
impl<'a> ChangesLookup<'a> {
|
||||
pub fn killed(&self, target: &CharacterId) -> Option<&'a DiedTo> {
|
||||
self.0.iter().find_map(|c| match c {
|
||||
NightChange::Kill { target: t, died_to } => (t == target).then_some(died_to),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> {
|
||||
self.0.iter().find_map(|c| match c {
|
||||
NightChange::Protection {
|
||||
target: t,
|
||||
protection,
|
||||
} => (t == target).then_some(protection),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn wolf_pack_kill_target(&self) -> Option<&'a CharacterId> {
|
||||
self.0.iter().find_map(|c| match c {
|
||||
NightChange::Kill {
|
||||
target,
|
||||
died_to: DiedTo::Wolfpack { night: _ },
|
||||
} => Some(target),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,6 +153,19 @@ impl Village {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn killing_wolf_id(&self) -> CharacterId {
|
||||
let wolves = self.living_wolf_pack_players();
|
||||
if let Some(ww) = wolves.iter().find(|w| matches!(w.role(), Role::Werewolf)) {
|
||||
ww.character_id().clone()
|
||||
} else if let Some(non_ss_wolf) = wolves.iter().find(|w| {
|
||||
w.role().wolf() && !matches!(w.role(), Role::Shapeshifter { shifted_into: _ })
|
||||
}) {
|
||||
non_ss_wolf.character_id().clone()
|
||||
} else {
|
||||
wolves.into_iter().next().unwrap().character_id().clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn living_players(&self) -> Box<[Target]> {
|
||||
self.characters
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ pub mod night;
|
|||
use core::{fmt::Display, num::NonZeroU8};
|
||||
|
||||
pub use ident::*;
|
||||
use night::{ActionPrompt, ActionResponse, ActionResult, RoleChange};
|
||||
use night::{ActionPrompt, ActionResponse, ActionResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ pub enum HostMessage {
|
|||
pub enum HostGameMessage {
|
||||
Day(HostDayMessage),
|
||||
Night(HostNightMessage),
|
||||
PreviousState,
|
||||
GetState,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,93 +2,107 @@ use serde::{Deserialize, Serialize};
|
|||
use werewolves_macros::ChecksAs;
|
||||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
message::PublicIdentity,
|
||||
player::CharacterId,
|
||||
role::{Alignment, PreviousGuardianAction, Role, RoleTitle},
|
||||
};
|
||||
|
||||
use super::Target;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ActionType {
|
||||
Protect = 0,
|
||||
WolfPackKill = 1,
|
||||
Direwolf = 2,
|
||||
Wolf = 3,
|
||||
Block = 4,
|
||||
Other = 5,
|
||||
RoleChange = 6,
|
||||
}
|
||||
|
||||
impl PartialOrd for ActionType {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
(*self as u8).partial_cmp(&(*other as u8))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs)]
|
||||
pub enum ActionPrompt {
|
||||
WolvesIntro {
|
||||
wolves: Box<[(Target, RoleTitle)]>,
|
||||
},
|
||||
RoleChange {
|
||||
new_role: RoleTitle,
|
||||
},
|
||||
Seer {
|
||||
living_players: Box<[Target]>,
|
||||
},
|
||||
Protector {
|
||||
targets: Box<[Target]>,
|
||||
},
|
||||
Arcanist {
|
||||
living_players: Box<[Target]>,
|
||||
},
|
||||
Gravedigger {
|
||||
dead_players: Box<[Target]>,
|
||||
},
|
||||
#[checks(ActionType::WolfPackKill)]
|
||||
WolvesIntro { wolves: Box<[(Target, RoleTitle)]> },
|
||||
#[checks(ActionType::RoleChange)]
|
||||
RoleChange { new_role: RoleTitle },
|
||||
#[checks(ActionType::Other)]
|
||||
Seer { living_players: Box<[Target]> },
|
||||
#[checks(ActionType::Protect)]
|
||||
Protector { targets: Box<[Target]> },
|
||||
#[checks(ActionType::Other)]
|
||||
Arcanist { living_players: Box<[Target]> },
|
||||
#[checks(ActionType::Other)]
|
||||
Gravedigger { dead_players: Box<[Target]> },
|
||||
#[checks(ActionType::Other)]
|
||||
Hunter {
|
||||
current_target: Option<Target>,
|
||||
living_players: Box<[Target]>,
|
||||
},
|
||||
Militia {
|
||||
living_players: Box<[Target]>,
|
||||
},
|
||||
#[checks(ActionType::Other)]
|
||||
Militia { living_players: Box<[Target]> },
|
||||
#[checks(ActionType::Other)]
|
||||
MapleWolf {
|
||||
kill_or_die: bool,
|
||||
living_players: Box<[Target]>,
|
||||
},
|
||||
#[checks(ActionType::Protect)]
|
||||
Guardian {
|
||||
previous: Option<PreviousGuardianAction>,
|
||||
living_players: Box<[Target]>,
|
||||
},
|
||||
WolfPackKill {
|
||||
living_villagers: Box<[Target]>,
|
||||
},
|
||||
#[checks(ActionType::Wolf)]
|
||||
WolfPackKill { living_villagers: Box<[Target]> },
|
||||
#[checks(ActionType::Wolf)]
|
||||
Shapeshifter,
|
||||
AlphaWolf {
|
||||
living_villagers: Box<[Target]>,
|
||||
},
|
||||
DireWolf {
|
||||
living_players: Box<[Target]>,
|
||||
},
|
||||
#[checks(ActionType::Wolf)]
|
||||
AlphaWolf { living_villagers: Box<[Target]> },
|
||||
#[checks(ActionType::Direwolf)]
|
||||
DireWolf { living_players: Box<[Target]> },
|
||||
}
|
||||
|
||||
impl PartialOrd for ActionPrompt {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
fn ordering_num(prompt: &ActionPrompt) -> u8 {
|
||||
match prompt {
|
||||
ActionPrompt::WolvesIntro { wolves: _ } => 0,
|
||||
ActionPrompt::Guardian {
|
||||
living_players: _,
|
||||
previous: _,
|
||||
}
|
||||
| ActionPrompt::Protector { targets: _ } => 1,
|
||||
ActionPrompt::WolfPackKill {
|
||||
living_villagers: _,
|
||||
} => 2,
|
||||
ActionPrompt::Shapeshifter => 3,
|
||||
ActionPrompt::AlphaWolf {
|
||||
living_villagers: _,
|
||||
} => 4,
|
||||
ActionPrompt::DireWolf { living_players: _ } => 5,
|
||||
ActionPrompt::Seer { living_players: _ }
|
||||
| ActionPrompt::Arcanist { living_players: _ }
|
||||
| ActionPrompt::Gravedigger { dead_players: _ }
|
||||
| ActionPrompt::Hunter {
|
||||
current_target: _,
|
||||
living_players: _,
|
||||
}
|
||||
| ActionPrompt::Militia { living_players: _ }
|
||||
| ActionPrompt::MapleWolf {
|
||||
kill_or_die: _,
|
||||
living_players: _,
|
||||
}
|
||||
| ActionPrompt::RoleChange { new_role: _ } => 0xFF,
|
||||
}
|
||||
}
|
||||
ordering_num(self).partial_cmp(&ordering_num(other))
|
||||
// fn ordering_num(prompt: &ActionPrompt) -> u8 {
|
||||
// match prompt {
|
||||
// ActionPrompt::WolvesIntro { wolves: _ } => 0,
|
||||
// ActionPrompt::Guardian {
|
||||
// living_players: _,
|
||||
// previous: _,
|
||||
// }
|
||||
// | ActionPrompt::Protector { targets: _ } => 1,
|
||||
// ActionPrompt::WolfPackKill {
|
||||
// living_villagers: _,
|
||||
// } => 2,
|
||||
// ActionPrompt::Shapeshifter => 3,
|
||||
// ActionPrompt::AlphaWolf {
|
||||
// living_villagers: _,
|
||||
// } => 4,
|
||||
// ActionPrompt::DireWolf { living_players: _ } => 5,
|
||||
// ActionPrompt::Seer { living_players: _ }
|
||||
// | ActionPrompt::Arcanist { living_players: _ }
|
||||
// | ActionPrompt::Gravedigger { dead_players: _ }
|
||||
// | ActionPrompt::Hunter {
|
||||
// current_target: _,
|
||||
// living_players: _,
|
||||
// }
|
||||
// | ActionPrompt::Militia { living_players: _ }
|
||||
// | ActionPrompt::MapleWolf {
|
||||
// kill_or_die: _,
|
||||
// living_players: _,
|
||||
// }
|
||||
// | ActionPrompt::RoleChange { new_role: _ } => 0xFF,
|
||||
// }
|
||||
// }
|
||||
// ordering_num(self).partial_cmp(&ordering_num(other))
|
||||
self.action_type().partial_cmp(&other.action_type())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -121,10 +135,3 @@ pub enum ActionResult {
|
|||
GoBackToSleep,
|
||||
WolvesIntroDone,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum RoleChange {
|
||||
Elder(Role),
|
||||
Apprentice(Role),
|
||||
Shapeshift(Role),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,5 +25,5 @@ ciborium = { version = "0.2", optional = true }
|
|||
colored = { version = "3.0" }
|
||||
|
||||
[features]
|
||||
# default = ["cbor"]
|
||||
default = ["cbor"]
|
||||
cbor = ["dep:ciborium"]
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ convert_case = { version = "0.8" }
|
|||
ciborium = { version = "0.2", optional = true }
|
||||
|
||||
[features]
|
||||
# default = ["cbor"]
|
||||
default = ["json"]
|
||||
default = ["cbor"]
|
||||
# default = ["json"]
|
||||
cbor = ["dep:ciborium"]
|
||||
json = ["dep:serde_json"]
|
||||
|
|
|
|||
|
|
@ -836,9 +836,18 @@ clients {
|
|||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
font-size: 2rem;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
&.margin-20 {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.margin-5 {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.gap {
|
||||
|
|
@ -1002,7 +1011,7 @@ error {
|
|||
background-color: $village_color;
|
||||
border: 3px solid darken($village_color, 20%);
|
||||
|
||||
&.on-the-block {
|
||||
&.marked {
|
||||
// background-color: brighten($village_color, 100%);
|
||||
filter: hue-rotate(90deg);
|
||||
}
|
||||
|
|
@ -1021,3 +1030,25 @@ error {
|
|||
filter: grayscale(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.binary {
|
||||
.button-container {
|
||||
background-color: $village_color;
|
||||
border: 3px solid darken($village_color, 20%);
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
|
||||
button {
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
align-self: center;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::components::Button;
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct BinaryChoiceProps {
|
||||
pub on_chosen: Callback<bool>,
|
||||
pub on_chosen: Option<Callback<bool>>,
|
||||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
}
|
||||
|
|
@ -17,15 +17,19 @@ pub fn BinaryChoice(
|
|||
}: &BinaryChoiceProps,
|
||||
) -> Html {
|
||||
let on_chosen_yes = on_chosen.clone();
|
||||
let yes = move |_| on_chosen_yes.emit(true);
|
||||
let yes = on_chosen_yes
|
||||
.map(|on_chosen| Callback::from(move |_| on_chosen.emit(true)))
|
||||
.unwrap_or_default();
|
||||
let on_chosen = on_chosen.clone();
|
||||
let no = move |_| on_chosen.emit(false);
|
||||
let no = on_chosen
|
||||
.map(|on_chosen| Callback::from(move |_| on_chosen.emit(false)))
|
||||
.unwrap_or_default();
|
||||
html! {
|
||||
<div class="column-list">
|
||||
<div class="column-list binary">
|
||||
{children.clone()}
|
||||
<div class="row-list">
|
||||
<Button on_click={yes}>{"yes"}</Button>
|
||||
<Button on_click={no}>{"no"}</Button>
|
||||
<div class="row-list gap">
|
||||
<Button on_click={yes}>{"Yes"}</Button>
|
||||
<Button on_click={no}>{"No"}</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ use werewolves_proto::{
|
|||
night::{ActionPrompt, ActionResponse},
|
||||
},
|
||||
player::CharacterId,
|
||||
role::PreviousGuardianAction,
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{
|
||||
Identity,
|
||||
action::{BinaryChoice, SingleTarget, TargetSelection, WolvesIntro},
|
||||
action::{BinaryChoice, OptionalSingleTarget, SingleTarget, TwoTarget, WolvesIntro},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
|
|
@ -54,7 +55,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
HostNightMessage::ActionResponse(ActionResponse::Seer(target)),
|
||||
)));
|
||||
})
|
||||
.into()
|
||||
});
|
||||
html! {
|
||||
<div>
|
||||
|
|
@ -96,7 +96,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
HostNightMessage::ActionResponse(ActionResponse::Protector(target)),
|
||||
)));
|
||||
})
|
||||
.into()
|
||||
});
|
||||
html! {
|
||||
<div>
|
||||
|
|
@ -108,7 +107,25 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
</div>
|
||||
}
|
||||
}
|
||||
ActionPrompt::Arcanist { living_players } => todo!(),
|
||||
ActionPrompt::Arcanist { 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>
|
||||
<TwoTarget
|
||||
targets={living_players.clone()}
|
||||
target_selection={on_select}
|
||||
headline={"arcanist"}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionPrompt::Gravedigger { dead_players } => {
|
||||
let on_complete = props.on_complete.clone();
|
||||
let on_select = props.big_screen.not().then(|| {
|
||||
|
|
@ -117,7 +134,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
HostNightMessage::ActionResponse(ActionResponse::Gravedigger(target)),
|
||||
)));
|
||||
})
|
||||
.into()
|
||||
});
|
||||
html! {
|
||||
<div>
|
||||
|
|
@ -132,16 +148,112 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
ActionPrompt::Hunter {
|
||||
current_target,
|
||||
living_players,
|
||||
} => todo!(),
|
||||
ActionPrompt::Militia { living_players } => todo!(),
|
||||
} => {
|
||||
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! {
|
||||
<SingleTarget
|
||||
targets={living_players.clone()}
|
||||
target_selection={on_select}
|
||||
headline={"hunter"}
|
||||
>
|
||||
<h3>
|
||||
<b>{"current target: "}</b>{current_target.clone().map(|t| html!{
|
||||
<Identity ident={t.public} />
|
||||
}).unwrap_or_else(|| html!{<i>{"none"}</i>})}
|
||||
</h3>
|
||||
</SingleTarget>
|
||||
}
|
||||
}
|
||||
ActionPrompt::Militia { 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! {
|
||||
<OptionalSingleTarget
|
||||
targets={living_players.clone()}
|
||||
target_selection={on_select}
|
||||
headline={"pew pew?"}
|
||||
/>
|
||||
}
|
||||
}
|
||||
ActionPrompt::MapleWolf {
|
||||
kill_or_die,
|
||||
living_players,
|
||||
} => todo!(),
|
||||
} => {
|
||||
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! {
|
||||
<OptionalSingleTarget
|
||||
targets={living_players.clone()}
|
||||
target_selection={on_select}
|
||||
headline={"nom nom?"}
|
||||
>
|
||||
{kill_or_die}
|
||||
</OptionalSingleTarget>
|
||||
}
|
||||
}
|
||||
ActionPrompt::Guardian {
|
||||
previous,
|
||||
living_players,
|
||||
} => todo!(),
|
||||
} => {
|
||||
let last_protect = previous.as_ref().map(|prev| match prev {
|
||||
PreviousGuardianAction::Protect(target) => {
|
||||
html! {
|
||||
<>
|
||||
<b>{"last night you protected: "}</b>
|
||||
<Identity ident={target.public.clone()}/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
PreviousGuardianAction::Guard(target) => html! {
|
||||
<>
|
||||
<b>{"last night you guarded: "}</b>
|
||||
<Identity ident={target.public.clone()}/>
|
||||
</>
|
||||
},
|
||||
});
|
||||
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)),
|
||||
)));
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<SingleTarget
|
||||
targets={living_players.clone()}
|
||||
target_selection={on_select}
|
||||
headline={"pick someone to protect"}
|
||||
>
|
||||
{last_protect}
|
||||
</SingleTarget>
|
||||
}
|
||||
}
|
||||
ActionPrompt::WolfPackKill { living_villagers } => {
|
||||
let on_complete = props.on_complete.clone();
|
||||
let on_select = props.big_screen.not().then(|| {
|
||||
|
|
@ -150,25 +262,24 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
HostNightMessage::ActionResponse(ActionResponse::WolfPackKillVote(target)),
|
||||
)));
|
||||
})
|
||||
.into()
|
||||
});
|
||||
html! {
|
||||
<div>
|
||||
<SingleTarget
|
||||
targets={living_villagers.clone()}
|
||||
target_selection={on_select}
|
||||
headline={"wolf pack kill"}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionPrompt::Shapeshifter => {
|
||||
let on_complete = props.on_complete.clone();
|
||||
let on_select = move |shift| {
|
||||
let on_select = props.big_screen.not().then_some({
|
||||
move |shift| {
|
||||
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||
HostNightMessage::ActionResponse(ActionResponse::Shapeshifter(shift)),
|
||||
)));
|
||||
};
|
||||
}
|
||||
});
|
||||
html! {
|
||||
<BinaryChoice on_chosen={on_select}>
|
||||
<h2>{"shapeshift?"}</h2>
|
||||
|
|
@ -183,10 +294,9 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
HostNightMessage::ActionResponse(ActionResponse::AlphaWolf(target)),
|
||||
)));
|
||||
})
|
||||
.into()
|
||||
});
|
||||
html! {
|
||||
<SingleTarget
|
||||
<OptionalSingleTarget
|
||||
targets={living_villagers.clone()}
|
||||
target_selection={on_select}
|
||||
headline={"alpha wolf target"}
|
||||
|
|
@ -201,7 +311,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
HostNightMessage::ActionResponse(ActionResponse::Direwolf(target)),
|
||||
)));
|
||||
})
|
||||
.into()
|
||||
});
|
||||
html! {
|
||||
<SingleTarget
|
||||
|
|
|
|||
|
|
@ -1,67 +1,56 @@
|
|||
use core::fmt::Debug;
|
||||
use core::{fmt::Debug, ops::Not};
|
||||
|
||||
use werewolves_macros::ChecksAs;
|
||||
use werewolves_proto::{message::Target, player::CharacterId};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::Identity;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum TargetSelection {
|
||||
SingleOptional(Callback<Option<CharacterId>>),
|
||||
Single(Callback<CharacterId>),
|
||||
}
|
||||
|
||||
impl From<Callback<CharacterId>> for TargetSelection {
|
||||
fn from(value: Callback<CharacterId>) -> Self {
|
||||
Self::Single(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Callback<Option<CharacterId>>> for TargetSelection {
|
||||
fn from(value: Callback<Option<CharacterId>>) -> Self {
|
||||
Self::SingleOptional(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TargetSelection {
|
||||
pub const fn button_disabled(&self, selected: &[CharacterId]) -> bool {
|
||||
match self {
|
||||
TargetSelection::SingleOptional(_) => selected.len() > 1,
|
||||
TargetSelection::Single(_) => selected.len() != 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::components::{Button, Identity};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct SingleTargetProps {
|
||||
pub struct TwoTargetProps {
|
||||
pub targets: Box<[Target]>,
|
||||
#[prop_or_default]
|
||||
pub headline: &'static str,
|
||||
#[prop_or_default]
|
||||
pub target_selection: Option<TargetSelection>,
|
||||
pub target_selection: Option<Callback<(CharacterId, CharacterId)>>,
|
||||
}
|
||||
|
||||
pub struct SingleTarget {
|
||||
selected: Vec<CharacterId>,
|
||||
#[derive(ChecksAs, Clone)]
|
||||
enum TwoTargetSelection {
|
||||
None,
|
||||
One(CharacterId),
|
||||
#[checks]
|
||||
Two(CharacterId, CharacterId),
|
||||
}
|
||||
|
||||
impl Component for SingleTarget {
|
||||
impl TwoTargetSelection {
|
||||
fn is_selected(&self, id: &CharacterId) -> bool {
|
||||
match self {
|
||||
TwoTargetSelection::None => false,
|
||||
TwoTargetSelection::One(character_id) => id == character_id,
|
||||
TwoTargetSelection::Two(cid1, cid2) => id == cid1 || id == cid2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TwoTarget(TwoTargetSelection);
|
||||
|
||||
impl Component for TwoTarget {
|
||||
type Message = CharacterId;
|
||||
|
||||
type Properties = SingleTargetProps;
|
||||
type Properties = TwoTargetProps;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Self {
|
||||
selected: Vec::new(),
|
||||
}
|
||||
Self(TwoTargetSelection::None)
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let SingleTargetProps {
|
||||
headline,
|
||||
let TwoTargetProps {
|
||||
targets,
|
||||
headline,
|
||||
target_selection,
|
||||
} = ctx.props();
|
||||
|
||||
let target_selection = target_selection.clone();
|
||||
let scope = ctx.link().clone();
|
||||
let card_select = Callback::from(move |target| {
|
||||
|
|
@ -73,44 +62,27 @@ impl Component for SingleTarget {
|
|||
html! {
|
||||
<TargetCard
|
||||
target={t.clone()}
|
||||
selected={self.selected.iter().any(|sel| sel == &t.character_id)}
|
||||
selected={self.0.is_selected(&t.character_id)}
|
||||
on_select={card_select.clone()}
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let headline = if headline.trim().is_empty() {
|
||||
html!()
|
||||
} else {
|
||||
html!(<h2>{headline}</h2>)
|
||||
};
|
||||
|
||||
let on_click =
|
||||
target_selection
|
||||
.as_ref()
|
||||
.and_then(|target_selection| match target_selection {
|
||||
TargetSelection::SingleOptional(on_click) => {
|
||||
if self.selected.len() > 1 {
|
||||
None
|
||||
} else {
|
||||
let selected = self.selected.first().cloned();
|
||||
let on_click = on_click.clone();
|
||||
Some(Callback::from(move |_| on_click.emit(selected.clone())))
|
||||
}
|
||||
}
|
||||
TargetSelection::Single(on_click) => {
|
||||
if self.selected.len() != 1 {
|
||||
None
|
||||
} else {
|
||||
let selected = self.selected[0].clone();
|
||||
let on_click = on_click.clone();
|
||||
Some(Callback::from(move |_| on_click.emit(selected.clone())))
|
||||
}
|
||||
}
|
||||
});
|
||||
let headline = headline
|
||||
.trim()
|
||||
.is_empty()
|
||||
.not()
|
||||
.then(|| html!(<h2>{headline}</h2>));
|
||||
|
||||
let submit = target_selection.as_ref().map(|target_selection| {
|
||||
let disabled = target_selection.button_disabled(&self.selected);
|
||||
let selected = match &self.0 {
|
||||
TwoTargetSelection::None | TwoTargetSelection::One(_) => None,
|
||||
TwoTargetSelection::Two(t1, t2) => Some((t1.clone(), t2.clone())),
|
||||
};
|
||||
let target_selection = target_selection.clone();
|
||||
let disabled = selected.is_none();
|
||||
let on_click =
|
||||
selected.map(|(t1, t2)| move |_| target_selection.emit((t1.clone(), t2.clone())));
|
||||
html! {
|
||||
<div class="button-container sp-ace">
|
||||
<button
|
||||
|
|
@ -135,15 +107,220 @@ impl Component for SingleTarget {
|
|||
}
|
||||
|
||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
if let Some(idx) = self
|
||||
.selected
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(idx, c)| (c == &msg).then_some(idx))
|
||||
{
|
||||
self.selected.swap_remove(idx);
|
||||
match &self.0 {
|
||||
TwoTargetSelection::None => self.0 = TwoTargetSelection::One(msg),
|
||||
TwoTargetSelection::One(character_id) => {
|
||||
if character_id == &msg {
|
||||
self.0 = TwoTargetSelection::None
|
||||
} else {
|
||||
self.selected.push(msg);
|
||||
self.0 = TwoTargetSelection::Two(character_id.clone(), msg)
|
||||
}
|
||||
}
|
||||
TwoTargetSelection::Two(t1, t2) => {
|
||||
if &msg == t1 {
|
||||
self.0 = TwoTargetSelection::One(t2.clone());
|
||||
} else if &msg == t2 {
|
||||
self.0 = TwoTargetSelection::One(t1.clone());
|
||||
} else {
|
||||
self.0 = TwoTargetSelection::Two(t1.clone(), msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct OptionalSingleTargetProps {
|
||||
pub targets: Box<[Target]>,
|
||||
#[prop_or_default]
|
||||
pub headline: &'static str,
|
||||
#[prop_or_default]
|
||||
pub target_selection: Option<Callback<Option<CharacterId>>>,
|
||||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
}
|
||||
|
||||
pub struct OptionalSingleTarget(Option<CharacterId>);
|
||||
|
||||
impl Component for OptionalSingleTarget {
|
||||
type Message = CharacterId;
|
||||
|
||||
type Properties = OptionalSingleTargetProps;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Self(None)
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let OptionalSingleTargetProps {
|
||||
targets,
|
||||
headline,
|
||||
target_selection,
|
||||
children,
|
||||
} = ctx.props();
|
||||
|
||||
let target_selection = target_selection.clone();
|
||||
let scope = ctx.link().clone();
|
||||
let card_select = Callback::from(move |target| {
|
||||
scope.send_message(target);
|
||||
});
|
||||
let targets = targets
|
||||
.iter()
|
||||
.map(|t| {
|
||||
html! {
|
||||
<TargetCard
|
||||
target={t.clone()}
|
||||
selected={self.0.as_ref().map(|c| c == &t.character_id).unwrap_or_default()}
|
||||
on_select={card_select.clone()}
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let headline = headline
|
||||
.trim()
|
||||
.is_empty()
|
||||
.not()
|
||||
.then(|| html!(<h2>{headline}</h2>));
|
||||
|
||||
let submit = target_selection.as_ref().map(|target_selection| {
|
||||
let target_selection = target_selection.clone();
|
||||
let sel = self.0.clone();
|
||||
let on_click = move |_| target_selection.emit(sel.clone());
|
||||
html! {
|
||||
<div class="button-container sp-ace">
|
||||
<button
|
||||
onclick={on_click}
|
||||
>
|
||||
{"submit"}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<div class="column-list">
|
||||
{headline}
|
||||
{children.clone()}
|
||||
<div class="row-list">
|
||||
{targets}
|
||||
</div>
|
||||
{submit}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match &self.0 {
|
||||
Some(t) => {
|
||||
if t == &msg {
|
||||
self.0 = None
|
||||
} else {
|
||||
self.0 = Some(msg);
|
||||
}
|
||||
}
|
||||
None => self.0 = Some(msg),
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct SingleTargetProps {
|
||||
pub targets: Box<[Target]>,
|
||||
#[prop_or_default]
|
||||
pub headline: &'static str,
|
||||
#[prop_or_default]
|
||||
pub target_selection: Option<Callback<CharacterId>>,
|
||||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
}
|
||||
|
||||
pub struct SingleTarget {
|
||||
selected: Option<CharacterId>,
|
||||
}
|
||||
|
||||
impl Component for SingleTarget {
|
||||
type Message = CharacterId;
|
||||
|
||||
type Properties = SingleTargetProps;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Self { selected: None }
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let SingleTargetProps {
|
||||
headline,
|
||||
targets,
|
||||
target_selection,
|
||||
children,
|
||||
} = ctx.props();
|
||||
let target_selection = target_selection.clone();
|
||||
let scope = ctx.link().clone();
|
||||
let card_select = Callback::from(move |target| {
|
||||
scope.send_message(target);
|
||||
});
|
||||
let targets = targets
|
||||
.iter()
|
||||
.map(|t| {
|
||||
html! {
|
||||
<TargetCard
|
||||
target={t.clone()}
|
||||
selected={self.selected.as_ref().map(|sel| sel == &t.character_id).unwrap_or_default()}
|
||||
on_select={card_select.clone()}
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let headline = headline
|
||||
.trim()
|
||||
.is_empty()
|
||||
.not()
|
||||
.then(|| html!(<h2>{headline}</h2>));
|
||||
|
||||
let submit = target_selection.as_ref().map(|target_selection| {
|
||||
let disabled = self.selected.is_none();
|
||||
let target_selection = target_selection.clone();
|
||||
let on_click = self
|
||||
.selected
|
||||
.clone()
|
||||
.map(|t| move |_| target_selection.emit(t.clone()));
|
||||
html! {
|
||||
<div class="button-container sp-ace">
|
||||
<button
|
||||
disabled={disabled}
|
||||
onclick={on_click}
|
||||
>
|
||||
{"submit"}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<div class="column-list">
|
||||
{headline}
|
||||
{children.clone()}
|
||||
<div class="row-list">
|
||||
{targets}
|
||||
</div>
|
||||
{submit}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match &self.selected {
|
||||
Some(current) => {
|
||||
if current == &msg {
|
||||
self.selected = None;
|
||||
} else {
|
||||
self.selected = Some(msg);
|
||||
}
|
||||
}
|
||||
None => self.selected = Some(msg),
|
||||
}
|
||||
true
|
||||
}
|
||||
|
|
@ -158,17 +335,24 @@ pub struct TargetCardProps {
|
|||
|
||||
#[function_component]
|
||||
fn TargetCard(props: &TargetCardProps) -> Html {
|
||||
let submenu = {
|
||||
let button_text = if props.selected { "unpick" } else { "pick" };
|
||||
let character_id = props.target.character_id.clone();
|
||||
let on_select = props.on_select.clone();
|
||||
let target = props.target.character_id.clone();
|
||||
let on_click = Callback::from(move |_| {
|
||||
on_select.emit(target.clone());
|
||||
});
|
||||
let on_click = Callback::from(move |_| on_select.emit(character_id.clone()));
|
||||
html! {
|
||||
<div
|
||||
class={classes!("target-card", "character", props.selected.then_some("selected"))}
|
||||
onclick={on_click}
|
||||
>
|
||||
<nav class="submenu">
|
||||
<Button on_click={on_click}>{button_text}</Button>
|
||||
</nav>
|
||||
}
|
||||
};
|
||||
let marked = props.selected.then_some("marked");
|
||||
html! {
|
||||
<div class={"row-list baseline margin-5"}>
|
||||
<div class={classes!("player", "ident", "column-list", marked)}>
|
||||
<Identity ident={props.target.public.clone()} />
|
||||
{submenu}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ pub fn DaytimePlayer(
|
|||
) -> Html {
|
||||
let dead = died_to.is_some().then_some("dead");
|
||||
let button_text = if *on_the_block { "unmark" } else { "mark" };
|
||||
let on_the_block = on_the_block.then_some("on-the-block");
|
||||
let on_the_block = on_the_block.then_some("marked");
|
||||
let submenu = died_to.is_none().then_some(()).and_then(|_| {
|
||||
on_select.as_ref().map(|on_select| {
|
||||
let character_id = character_id.clone();
|
||||
|
|
|
|||
|
|
@ -469,10 +469,15 @@ impl Component for Host {
|
|||
HostMessage::Echo(ServerToHostMessage::Error(GameError::NoApprenticeMentor)),
|
||||
self.send.clone(),
|
||||
);
|
||||
let on_prev_click = callback::send_message(
|
||||
HostMessage::InGame(HostGameMessage::PreviousState),
|
||||
self.send.clone(),
|
||||
);
|
||||
html! {
|
||||
<nav class="debug-nav">
|
||||
<nav class="debug-nav" style="z-index: 10;">
|
||||
<div class="row-list">
|
||||
<Button on_click={on_error_click}>{"error"}</Button>
|
||||
<Button on_click={on_prev_click}>{"previous"}</Button>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue