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 core::{fmt::Debug, num::NonZeroU8};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use werewolves_macros::Extract;
|
||||||
|
|
||||||
use crate::{game::DateTime, player::CharacterId};
|
use crate::{game::DateTime, player::CharacterId};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Extract)]
|
||||||
pub enum DiedTo {
|
pub enum DiedTo {
|
||||||
Execution {
|
Execution {
|
||||||
day: NonZeroU8,
|
day: NonZeroU8,
|
||||||
},
|
},
|
||||||
|
#[extract(source as killer)]
|
||||||
MapleWolf {
|
MapleWolf {
|
||||||
source: CharacterId,
|
source: CharacterId,
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
|
|
@ -17,13 +19,17 @@ pub enum DiedTo {
|
||||||
MapleWolfStarved {
|
MapleWolfStarved {
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
},
|
},
|
||||||
|
#[extract(killer as killer)]
|
||||||
Militia {
|
Militia {
|
||||||
killer: CharacterId,
|
killer: CharacterId,
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
},
|
},
|
||||||
|
#[extract(killing_wolf as killer)]
|
||||||
Wolfpack {
|
Wolfpack {
|
||||||
|
killing_wolf: CharacterId,
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
},
|
},
|
||||||
|
#[extract(killer as killer)]
|
||||||
AlphaWolf {
|
AlphaWolf {
|
||||||
killer: CharacterId,
|
killer: CharacterId,
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
|
|
@ -32,12 +38,17 @@ pub enum DiedTo {
|
||||||
into: CharacterId,
|
into: CharacterId,
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
},
|
},
|
||||||
|
#[extract(killer as killer)]
|
||||||
Hunter {
|
Hunter {
|
||||||
killer: CharacterId,
|
killer: CharacterId,
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
},
|
},
|
||||||
Guardian {
|
#[extract(source as killer)]
|
||||||
killer: CharacterId,
|
GuardianProtecting {
|
||||||
|
source: CharacterId,
|
||||||
|
protecting: CharacterId,
|
||||||
|
protecting_from: CharacterId,
|
||||||
|
protecting_from_cause: Box<DiedTo>,
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +58,13 @@ impl DiedTo {
|
||||||
match self {
|
match self {
|
||||||
DiedTo::Execution { day } => DateTime::Day { number: *day },
|
DiedTo::Execution { day } => DateTime::Day { number: *day },
|
||||||
|
|
||||||
DiedTo::Guardian { killer: _, night }
|
DiedTo::GuardianProtecting {
|
||||||
|
source: _,
|
||||||
|
protecting: _,
|
||||||
|
protecting_from: _,
|
||||||
|
protecting_from_cause: _,
|
||||||
|
night,
|
||||||
|
}
|
||||||
| DiedTo::MapleWolf {
|
| DiedTo::MapleWolf {
|
||||||
source: _,
|
source: _,
|
||||||
night,
|
night,
|
||||||
|
|
@ -55,7 +72,10 @@ impl DiedTo {
|
||||||
}
|
}
|
||||||
| DiedTo::MapleWolfStarved { night }
|
| DiedTo::MapleWolfStarved { night }
|
||||||
| DiedTo::Militia { killer: _, night }
|
| DiedTo::Militia { killer: _, night }
|
||||||
| DiedTo::Wolfpack { night }
|
| DiedTo::Wolfpack {
|
||||||
|
night,
|
||||||
|
killing_wolf: _,
|
||||||
|
}
|
||||||
| DiedTo::AlphaWolf { killer: _, night }
|
| DiedTo::AlphaWolf { killer: _, night }
|
||||||
| DiedTo::Shapeshift { into: _, night }
|
| DiedTo::Shapeshift { into: _, night }
|
||||||
| DiedTo::Hunter { killer: _, night } => DateTime::Night {
|
| DiedTo::Hunter { killer: _, night } => DateTime::Night {
|
||||||
|
|
|
||||||
|
|
@ -69,4 +69,8 @@ pub enum GameError {
|
||||||
GameOngoing,
|
GameOngoing,
|
||||||
#[error("needs a role reveal")]
|
#[error("needs a role reveal")]
|
||||||
NeedRoleReveal,
|
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 night;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod village;
|
mod village;
|
||||||
|
|
@ -27,12 +28,14 @@ type Result<T> = core::result::Result<T, GameError>;
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
previous: Vec<GameState>,
|
previous: Vec<GameState>,
|
||||||
|
next: Vec<GameState>,
|
||||||
state: GameState,
|
state: GameState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
|
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
next: Vec::new(),
|
||||||
previous: Vec::new(),
|
previous: Vec::new(),
|
||||||
state: GameState::Night {
|
state: GameState::Night {
|
||||||
night: Night::new(Village::new(players, settings)?)?,
|
night: Night::new(Village::new(players, settings)?)?,
|
||||||
|
|
@ -161,6 +164,24 @@ impl Game {
|
||||||
},
|
},
|
||||||
HostGameMessage::Night(_),
|
HostGameMessage::Night(_),
|
||||||
) => Err(GameError::InvalidMessageForGameState),
|
) => 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::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{DateTime, Village},
|
game::{
|
||||||
|
DateTime, Village,
|
||||||
|
kill::{self, ChangesLookup},
|
||||||
|
},
|
||||||
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
||||||
player::{Character, CharacterId, Protection},
|
player::{Character, CharacterId, Protection},
|
||||||
role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle},
|
role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle},
|
||||||
|
|
@ -19,6 +22,7 @@ pub struct Night {
|
||||||
village: Village,
|
village: Village,
|
||||||
night: u8,
|
night: u8,
|
||||||
action_queue: VecDeque<(ActionPrompt, Character)>,
|
action_queue: VecDeque<(ActionPrompt, Character)>,
|
||||||
|
used_actions: Vec<(ActionPrompt, Character)>,
|
||||||
changes: Vec<NightChange>,
|
changes: Vec<NightChange>,
|
||||||
night_state: NightState,
|
night_state: NightState,
|
||||||
}
|
}
|
||||||
|
|
@ -166,15 +170,62 @@ impl Night {
|
||||||
village,
|
village,
|
||||||
night_state,
|
night_state,
|
||||||
action_queue,
|
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> {
|
pub fn collect_completed(&self) -> Result<Village> {
|
||||||
if !matches!(self.night_state, NightState::Complete) {
|
if !matches!(self.night_state, NightState::Complete) {
|
||||||
return Err(GameError::NotEndOfNight);
|
return Err(GameError::NotEndOfNight);
|
||||||
}
|
}
|
||||||
let mut new_village = self.village.clone();
|
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() {
|
for change in self.changes.iter() {
|
||||||
match change {
|
match change {
|
||||||
NightChange::RoleChange(character_id, role_title) => new_village
|
NightChange::RoleChange(character_id, role_title) => new_village
|
||||||
|
|
@ -201,108 +252,17 @@ impl Night {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NightChange::Kill { target, died_to } => {
|
NightChange::Kill { target, died_to } => {
|
||||||
if let DiedTo::MapleWolf {
|
if let Some(kill) = kill::resolve_kill(
|
||||||
source,
|
&mut changes,
|
||||||
night,
|
target,
|
||||||
starves_if_fails: true,
|
died_to,
|
||||||
} = died_to
|
self.night,
|
||||||
&& changes.protected(target).is_some()
|
&self.village,
|
||||||
{
|
)? {
|
||||||
// kill maple first, then act as if they get their kill attempt
|
kill.apply_to_village(&mut new_village)?;
|
||||||
new_village
|
|
||||||
.character_by_id_mut(source)
|
|
||||||
.unwrap()
|
|
||||||
.kill(DiedTo::MapleWolfStarved { night: *night });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 } => {
|
NightChange::Shapeshift { source } => {
|
||||||
// TODO: shapeshift should probably notify immediately after it happens
|
|
||||||
if let Some(target) = changes.wolf_pack_kill_target()
|
if let Some(target) = changes.wolf_pack_kill_target()
|
||||||
&& changes.protected(target).is_none()
|
&& changes.protected(target).is_none()
|
||||||
{
|
{
|
||||||
|
|
@ -317,13 +277,15 @@ impl Night {
|
||||||
into: target.clone(),
|
into: target.clone(),
|
||||||
night: NonZeroU8::new(self.night).unwrap(),
|
night: NonZeroU8::new(self.night).unwrap(),
|
||||||
});
|
});
|
||||||
let target = new_village.find_by_character_id_mut(target).unwrap();
|
// role change pushed in [apply_shapeshift]
|
||||||
target
|
|
||||||
.role_change(
|
// let target = new_village.find_by_character_id_mut(target).unwrap();
|
||||||
RoleTitle::Werewolf,
|
// target
|
||||||
DateTime::Night { number: self.night },
|
// .role_change(
|
||||||
)
|
// RoleTitle::Werewolf,
|
||||||
.unwrap();
|
// DateTime::Night { number: self.night },
|
||||||
|
// )
|
||||||
|
// .unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NightChange::RoleBlock {
|
NightChange::RoleBlock {
|
||||||
|
|
@ -347,7 +309,11 @@ impl Night {
|
||||||
if let Some(kill_target) = self.changes.iter().find_map(|c| match c {
|
if let Some(kill_target) = self.changes.iter().find_map(|c| match c {
|
||||||
NightChange::Kill {
|
NightChange::Kill {
|
||||||
target,
|
target,
|
||||||
died_to: DiedTo::Wolfpack { night: _ },
|
died_to:
|
||||||
|
DiedTo::Wolfpack {
|
||||||
|
night: _,
|
||||||
|
killing_wolf: _,
|
||||||
|
},
|
||||||
} => Some(target.clone()),
|
} => Some(target.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -361,22 +327,42 @@ impl Night {
|
||||||
// there is protection, so the kill doesn't happen -> no shapeshift
|
// there is protection, so the kill doesn't happen -> no shapeshift
|
||||||
return Ok(ActionResult::GoBackToSleep);
|
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!(
|
matches!(
|
||||||
c,
|
c,
|
||||||
NightChange::Kill {
|
NightChange::Kill {
|
||||||
target: _,
|
target: _,
|
||||||
died_to: DiedTo::Wolfpack { night: _ }
|
died_to: DiedTo::Wolfpack {
|
||||||
|
night: _,
|
||||||
|
killing_wolf: _
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}) {
|
}) {
|
||||||
*kill = NightChange::Kill {
|
self.changes.push(NightChange::Kill {
|
||||||
target: source.clone(),
|
target: source.clone(),
|
||||||
died_to: DiedTo::Shapeshift {
|
died_to: DiedTo::Shapeshift {
|
||||||
into: kill_target.clone(),
|
into: kill_target.clone(),
|
||||||
night: NonZeroU8::new(self.night).unwrap(),
|
night: NonZeroU8::new(self.night).unwrap(),
|
||||||
},
|
},
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
self.changes.push(NightChange::Shapeshift {
|
self.changes.push(NightChange::Shapeshift {
|
||||||
source: source.clone(),
|
source: source.clone(),
|
||||||
|
|
@ -780,7 +766,10 @@ impl Night {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change: Some(NightChange::Kill {
|
change: Some(NightChange::Kill {
|
||||||
target: target.clone(),
|
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)),
|
unless: Some(Unless::TargetBlocked(target)),
|
||||||
})
|
})
|
||||||
|
|
@ -980,6 +969,7 @@ impl Night {
|
||||||
NightState::Complete => return Err(GameError::NightOver),
|
NightState::Complete => return Err(GameError::NightOver),
|
||||||
}
|
}
|
||||||
if let Some((prompt, character)) = self.action_queue.pop_front() {
|
if let Some((prompt, character)) = self.action_queue.pop_front() {
|
||||||
|
self.used_actions.push((prompt.clone(), character.clone()));
|
||||||
self.night_state = NightState::Active {
|
self.night_state = NightState::Active {
|
||||||
current_prompt: prompt,
|
current_prompt: prompt,
|
||||||
current_char: character.character_id().clone(),
|
current_char: character.character_id().clone(),
|
||||||
|
|
@ -996,34 +986,3 @@ impl Night {
|
||||||
self.changes.as_slice()
|
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()
|
.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]> {
|
pub fn living_players(&self) -> Box<[Target]> {
|
||||||
self.characters
|
self.characters
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ pub mod night;
|
||||||
use core::{fmt::Display, num::NonZeroU8};
|
use core::{fmt::Display, num::NonZeroU8};
|
||||||
|
|
||||||
pub use ident::*;
|
pub use ident::*;
|
||||||
use night::{ActionPrompt, ActionResponse, ActionResult, RoleChange};
|
use night::{ActionPrompt, ActionResponse, ActionResult};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ pub enum HostMessage {
|
||||||
pub enum HostGameMessage {
|
pub enum HostGameMessage {
|
||||||
Day(HostDayMessage),
|
Day(HostDayMessage),
|
||||||
Night(HostNightMessage),
|
Night(HostNightMessage),
|
||||||
|
PreviousState,
|
||||||
GetState,
|
GetState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,93 +2,107 @@ use serde::{Deserialize, Serialize};
|
||||||
use werewolves_macros::ChecksAs;
|
use werewolves_macros::ChecksAs;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
diedto::DiedTo,
|
||||||
|
message::PublicIdentity,
|
||||||
player::CharacterId,
|
player::CharacterId,
|
||||||
role::{Alignment, PreviousGuardianAction, Role, RoleTitle},
|
role::{Alignment, PreviousGuardianAction, Role, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Target;
|
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 {
|
pub enum ActionPrompt {
|
||||||
WolvesIntro {
|
#[checks(ActionType::WolfPackKill)]
|
||||||
wolves: Box<[(Target, RoleTitle)]>,
|
WolvesIntro { wolves: Box<[(Target, RoleTitle)]> },
|
||||||
},
|
#[checks(ActionType::RoleChange)]
|
||||||
RoleChange {
|
RoleChange { new_role: RoleTitle },
|
||||||
new_role: RoleTitle,
|
#[checks(ActionType::Other)]
|
||||||
},
|
Seer { living_players: Box<[Target]> },
|
||||||
Seer {
|
#[checks(ActionType::Protect)]
|
||||||
living_players: Box<[Target]>,
|
Protector { targets: Box<[Target]> },
|
||||||
},
|
#[checks(ActionType::Other)]
|
||||||
Protector {
|
Arcanist { living_players: Box<[Target]> },
|
||||||
targets: Box<[Target]>,
|
#[checks(ActionType::Other)]
|
||||||
},
|
Gravedigger { dead_players: Box<[Target]> },
|
||||||
Arcanist {
|
#[checks(ActionType::Other)]
|
||||||
living_players: Box<[Target]>,
|
|
||||||
},
|
|
||||||
Gravedigger {
|
|
||||||
dead_players: Box<[Target]>,
|
|
||||||
},
|
|
||||||
Hunter {
|
Hunter {
|
||||||
current_target: Option<Target>,
|
current_target: Option<Target>,
|
||||||
living_players: Box<[Target]>,
|
living_players: Box<[Target]>,
|
||||||
},
|
},
|
||||||
Militia {
|
#[checks(ActionType::Other)]
|
||||||
living_players: Box<[Target]>,
|
Militia { living_players: Box<[Target]> },
|
||||||
},
|
#[checks(ActionType::Other)]
|
||||||
MapleWolf {
|
MapleWolf {
|
||||||
kill_or_die: bool,
|
kill_or_die: bool,
|
||||||
living_players: Box<[Target]>,
|
living_players: Box<[Target]>,
|
||||||
},
|
},
|
||||||
|
#[checks(ActionType::Protect)]
|
||||||
Guardian {
|
Guardian {
|
||||||
previous: Option<PreviousGuardianAction>,
|
previous: Option<PreviousGuardianAction>,
|
||||||
living_players: Box<[Target]>,
|
living_players: Box<[Target]>,
|
||||||
},
|
},
|
||||||
WolfPackKill {
|
#[checks(ActionType::Wolf)]
|
||||||
living_villagers: Box<[Target]>,
|
WolfPackKill { living_villagers: Box<[Target]> },
|
||||||
},
|
#[checks(ActionType::Wolf)]
|
||||||
Shapeshifter,
|
Shapeshifter,
|
||||||
AlphaWolf {
|
#[checks(ActionType::Wolf)]
|
||||||
living_villagers: Box<[Target]>,
|
AlphaWolf { living_villagers: Box<[Target]> },
|
||||||
},
|
#[checks(ActionType::Direwolf)]
|
||||||
DireWolf {
|
DireWolf { living_players: Box<[Target]> },
|
||||||
living_players: Box<[Target]>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for ActionPrompt {
|
impl PartialOrd for ActionPrompt {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
fn ordering_num(prompt: &ActionPrompt) -> u8 {
|
// fn ordering_num(prompt: &ActionPrompt) -> u8 {
|
||||||
match prompt {
|
// match prompt {
|
||||||
ActionPrompt::WolvesIntro { wolves: _ } => 0,
|
// ActionPrompt::WolvesIntro { wolves: _ } => 0,
|
||||||
ActionPrompt::Guardian {
|
// ActionPrompt::Guardian {
|
||||||
living_players: _,
|
// living_players: _,
|
||||||
previous: _,
|
// previous: _,
|
||||||
}
|
// }
|
||||||
| ActionPrompt::Protector { targets: _ } => 1,
|
// | ActionPrompt::Protector { targets: _ } => 1,
|
||||||
ActionPrompt::WolfPackKill {
|
// ActionPrompt::WolfPackKill {
|
||||||
living_villagers: _,
|
// living_villagers: _,
|
||||||
} => 2,
|
// } => 2,
|
||||||
ActionPrompt::Shapeshifter => 3,
|
// ActionPrompt::Shapeshifter => 3,
|
||||||
ActionPrompt::AlphaWolf {
|
// ActionPrompt::AlphaWolf {
|
||||||
living_villagers: _,
|
// living_villagers: _,
|
||||||
} => 4,
|
// } => 4,
|
||||||
ActionPrompt::DireWolf { living_players: _ } => 5,
|
// ActionPrompt::DireWolf { living_players: _ } => 5,
|
||||||
ActionPrompt::Seer { living_players: _ }
|
// ActionPrompt::Seer { living_players: _ }
|
||||||
| ActionPrompt::Arcanist { living_players: _ }
|
// | ActionPrompt::Arcanist { living_players: _ }
|
||||||
| ActionPrompt::Gravedigger { dead_players: _ }
|
// | ActionPrompt::Gravedigger { dead_players: _ }
|
||||||
| ActionPrompt::Hunter {
|
// | ActionPrompt::Hunter {
|
||||||
current_target: _,
|
// current_target: _,
|
||||||
living_players: _,
|
// living_players: _,
|
||||||
}
|
// }
|
||||||
| ActionPrompt::Militia { living_players: _ }
|
// | ActionPrompt::Militia { living_players: _ }
|
||||||
| ActionPrompt::MapleWolf {
|
// | ActionPrompt::MapleWolf {
|
||||||
kill_or_die: _,
|
// kill_or_die: _,
|
||||||
living_players: _,
|
// living_players: _,
|
||||||
}
|
// }
|
||||||
| ActionPrompt::RoleChange { new_role: _ } => 0xFF,
|
// | ActionPrompt::RoleChange { new_role: _ } => 0xFF,
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
ordering_num(self).partial_cmp(&ordering_num(other))
|
// ordering_num(self).partial_cmp(&ordering_num(other))
|
||||||
|
self.action_type().partial_cmp(&other.action_type())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,10 +135,3 @@ pub enum ActionResult {
|
||||||
GoBackToSleep,
|
GoBackToSleep,
|
||||||
WolvesIntroDone,
|
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" }
|
colored = { version = "3.0" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# default = ["cbor"]
|
default = ["cbor"]
|
||||||
cbor = ["dep:ciborium"]
|
cbor = ["dep:ciborium"]
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ convert_case = { version = "0.8" }
|
||||||
ciborium = { version = "0.2", optional = true }
|
ciborium = { version = "0.2", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# default = ["cbor"]
|
default = ["cbor"]
|
||||||
default = ["json"]
|
# default = ["json"]
|
||||||
cbor = ["dep:ciborium"]
|
cbor = ["dep:ciborium"]
|
||||||
json = ["dep:serde_json"]
|
json = ["dep:serde_json"]
|
||||||
|
|
|
||||||
|
|
@ -836,9 +836,18 @@ clients {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&.margin-20 {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
justify-content: center;
|
}
|
||||||
|
|
||||||
|
&.margin-5 {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gap {
|
.gap {
|
||||||
|
|
@ -1002,7 +1011,7 @@ error {
|
||||||
background-color: $village_color;
|
background-color: $village_color;
|
||||||
border: 3px solid darken($village_color, 20%);
|
border: 3px solid darken($village_color, 20%);
|
||||||
|
|
||||||
&.on-the-block {
|
&.marked {
|
||||||
// background-color: brighten($village_color, 100%);
|
// background-color: brighten($village_color, 100%);
|
||||||
filter: hue-rotate(90deg);
|
filter: hue-rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
@ -1021,3 +1030,25 @@ error {
|
||||||
filter: grayscale(100%);
|
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)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct BinaryChoiceProps {
|
pub struct BinaryChoiceProps {
|
||||||
pub on_chosen: Callback<bool>,
|
pub on_chosen: Option<Callback<bool>>,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub children: Html,
|
pub children: Html,
|
||||||
}
|
}
|
||||||
|
|
@ -17,15 +17,19 @@ pub fn BinaryChoice(
|
||||||
}: &BinaryChoiceProps,
|
}: &BinaryChoiceProps,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
let on_chosen_yes = on_chosen.clone();
|
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 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! {
|
html! {
|
||||||
<div class="column-list">
|
<div class="column-list binary">
|
||||||
{children.clone()}
|
{children.clone()}
|
||||||
<div class="row-list">
|
<div class="row-list gap">
|
||||||
<Button on_click={yes}>{"yes"}</Button>
|
<Button on_click={yes}>{"Yes"}</Button>
|
||||||
<Button on_click={no}>{"no"}</Button>
|
<Button on_click={no}>{"No"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,13 @@ use werewolves_proto::{
|
||||||
night::{ActionPrompt, ActionResponse},
|
night::{ActionPrompt, ActionResponse},
|
||||||
},
|
},
|
||||||
player::CharacterId,
|
player::CharacterId,
|
||||||
|
role::PreviousGuardianAction,
|
||||||
};
|
};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
Identity,
|
Identity,
|
||||||
action::{BinaryChoice, SingleTarget, TargetSelection, WolvesIntro},
|
action::{BinaryChoice, OptionalSingleTarget, SingleTarget, TwoTarget, WolvesIntro},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
|
@ -54,7 +55,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
HostNightMessage::ActionResponse(ActionResponse::Seer(target)),
|
HostNightMessage::ActionResponse(ActionResponse::Seer(target)),
|
||||||
)));
|
)));
|
||||||
})
|
})
|
||||||
.into()
|
|
||||||
});
|
});
|
||||||
html! {
|
html! {
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -96,7 +96,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
HostNightMessage::ActionResponse(ActionResponse::Protector(target)),
|
HostNightMessage::ActionResponse(ActionResponse::Protector(target)),
|
||||||
)));
|
)));
|
||||||
})
|
})
|
||||||
.into()
|
|
||||||
});
|
});
|
||||||
html! {
|
html! {
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -108,7 +107,25 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
</div>
|
</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 } => {
|
ActionPrompt::Gravedigger { dead_players } => {
|
||||||
let on_complete = props.on_complete.clone();
|
let on_complete = props.on_complete.clone();
|
||||||
let on_select = props.big_screen.not().then(|| {
|
let on_select = props.big_screen.not().then(|| {
|
||||||
|
|
@ -117,7 +134,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
HostNightMessage::ActionResponse(ActionResponse::Gravedigger(target)),
|
HostNightMessage::ActionResponse(ActionResponse::Gravedigger(target)),
|
||||||
)));
|
)));
|
||||||
})
|
})
|
||||||
.into()
|
|
||||||
});
|
});
|
||||||
html! {
|
html! {
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -132,16 +148,112 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
ActionPrompt::Hunter {
|
ActionPrompt::Hunter {
|
||||||
current_target,
|
current_target,
|
||||||
living_players,
|
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 {
|
ActionPrompt::MapleWolf {
|
||||||
kill_or_die,
|
kill_or_die,
|
||||||
living_players,
|
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 {
|
ActionPrompt::Guardian {
|
||||||
previous,
|
previous,
|
||||||
living_players,
|
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 } => {
|
ActionPrompt::WolfPackKill { living_villagers } => {
|
||||||
let on_complete = props.on_complete.clone();
|
let on_complete = props.on_complete.clone();
|
||||||
let on_select = props.big_screen.not().then(|| {
|
let on_select = props.big_screen.not().then(|| {
|
||||||
|
|
@ -150,25 +262,24 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
HostNightMessage::ActionResponse(ActionResponse::WolfPackKillVote(target)),
|
HostNightMessage::ActionResponse(ActionResponse::WolfPackKillVote(target)),
|
||||||
)));
|
)));
|
||||||
})
|
})
|
||||||
.into()
|
|
||||||
});
|
});
|
||||||
html! {
|
html! {
|
||||||
<div>
|
|
||||||
<SingleTarget
|
<SingleTarget
|
||||||
targets={living_villagers.clone()}
|
targets={living_villagers.clone()}
|
||||||
target_selection={on_select}
|
target_selection={on_select}
|
||||||
headline={"wolf pack kill"}
|
headline={"wolf pack kill"}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ActionPrompt::Shapeshifter => {
|
ActionPrompt::Shapeshifter => {
|
||||||
let on_complete = props.on_complete.clone();
|
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(
|
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||||
HostNightMessage::ActionResponse(ActionResponse::Shapeshifter(shift)),
|
HostNightMessage::ActionResponse(ActionResponse::Shapeshifter(shift)),
|
||||||
)));
|
)));
|
||||||
};
|
}
|
||||||
|
});
|
||||||
html! {
|
html! {
|
||||||
<BinaryChoice on_chosen={on_select}>
|
<BinaryChoice on_chosen={on_select}>
|
||||||
<h2>{"shapeshift?"}</h2>
|
<h2>{"shapeshift?"}</h2>
|
||||||
|
|
@ -183,10 +294,9 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
HostNightMessage::ActionResponse(ActionResponse::AlphaWolf(target)),
|
HostNightMessage::ActionResponse(ActionResponse::AlphaWolf(target)),
|
||||||
)));
|
)));
|
||||||
})
|
})
|
||||||
.into()
|
|
||||||
});
|
});
|
||||||
html! {
|
html! {
|
||||||
<SingleTarget
|
<OptionalSingleTarget
|
||||||
targets={living_villagers.clone()}
|
targets={living_villagers.clone()}
|
||||||
target_selection={on_select}
|
target_selection={on_select}
|
||||||
headline={"alpha wolf target"}
|
headline={"alpha wolf target"}
|
||||||
|
|
@ -201,7 +311,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
HostNightMessage::ActionResponse(ActionResponse::Direwolf(target)),
|
HostNightMessage::ActionResponse(ActionResponse::Direwolf(target)),
|
||||||
)));
|
)));
|
||||||
})
|
})
|
||||||
.into()
|
|
||||||
});
|
});
|
||||||
html! {
|
html! {
|
||||||
<SingleTarget
|
<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 werewolves_proto::{message::Target, player::CharacterId};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::Identity;
|
use crate::components::{Button, 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct SingleTargetProps {
|
pub struct TwoTargetProps {
|
||||||
pub targets: Box<[Target]>,
|
pub targets: Box<[Target]>,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub headline: &'static str,
|
pub headline: &'static str,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub target_selection: Option<TargetSelection>,
|
pub target_selection: Option<Callback<(CharacterId, CharacterId)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SingleTarget {
|
#[derive(ChecksAs, Clone)]
|
||||||
selected: Vec<CharacterId>,
|
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 Message = CharacterId;
|
||||||
|
|
||||||
type Properties = SingleTargetProps;
|
type Properties = TwoTargetProps;
|
||||||
|
|
||||||
fn create(_: &Context<Self>) -> Self {
|
fn create(_: &Context<Self>) -> Self {
|
||||||
Self {
|
Self(TwoTargetSelection::None)
|
||||||
selected: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
let SingleTargetProps {
|
let TwoTargetProps {
|
||||||
headline,
|
|
||||||
targets,
|
targets,
|
||||||
|
headline,
|
||||||
target_selection,
|
target_selection,
|
||||||
} = ctx.props();
|
} = ctx.props();
|
||||||
|
|
||||||
let target_selection = target_selection.clone();
|
let target_selection = target_selection.clone();
|
||||||
let scope = ctx.link().clone();
|
let scope = ctx.link().clone();
|
||||||
let card_select = Callback::from(move |target| {
|
let card_select = Callback::from(move |target| {
|
||||||
|
|
@ -73,44 +62,27 @@ impl Component for SingleTarget {
|
||||||
html! {
|
html! {
|
||||||
<TargetCard
|
<TargetCard
|
||||||
target={t.clone()}
|
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()}
|
on_select={card_select.clone()}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Html>();
|
.collect::<Html>();
|
||||||
let headline = if headline.trim().is_empty() {
|
let headline = headline
|
||||||
html!()
|
.trim()
|
||||||
} else {
|
.is_empty()
|
||||||
html!(<h2>{headline}</h2>)
|
.not()
|
||||||
};
|
.then(|| 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 submit = target_selection.as_ref().map(|target_selection| {
|
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! {
|
html! {
|
||||||
<div class="button-container sp-ace">
|
<div class="button-container sp-ace">
|
||||||
<button
|
<button
|
||||||
|
|
@ -135,15 +107,220 @@ impl Component for SingleTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
if let Some(idx) = self
|
match &self.0 {
|
||||||
.selected
|
TwoTargetSelection::None => self.0 = TwoTargetSelection::One(msg),
|
||||||
.iter()
|
TwoTargetSelection::One(character_id) => {
|
||||||
.enumerate()
|
if character_id == &msg {
|
||||||
.find_map(|(idx, c)| (c == &msg).then_some(idx))
|
self.0 = TwoTargetSelection::None
|
||||||
{
|
|
||||||
self.selected.swap_remove(idx);
|
|
||||||
} else {
|
} 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
|
true
|
||||||
}
|
}
|
||||||
|
|
@ -158,17 +335,24 @@ pub struct TargetCardProps {
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
fn TargetCard(props: &TargetCardProps) -> Html {
|
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 on_select = props.on_select.clone();
|
||||||
let target = props.target.character_id.clone();
|
let on_click = Callback::from(move |_| on_select.emit(character_id.clone()));
|
||||||
let on_click = Callback::from(move |_| {
|
|
||||||
on_select.emit(target.clone());
|
|
||||||
});
|
|
||||||
html! {
|
html! {
|
||||||
<div
|
<nav class="submenu">
|
||||||
class={classes!("target-card", "character", props.selected.then_some("selected"))}
|
<Button on_click={on_click}>{button_text}</Button>
|
||||||
onclick={on_click}
|
</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()} />
|
<Identity ident={props.target.public.clone()} />
|
||||||
|
{submenu}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ pub fn DaytimePlayer(
|
||||||
) -> Html {
|
) -> Html {
|
||||||
let dead = died_to.is_some().then_some("dead");
|
let dead = died_to.is_some().then_some("dead");
|
||||||
let button_text = if *on_the_block { "unmark" } else { "mark" };
|
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(|_| {
|
let submenu = died_to.is_none().then_some(()).and_then(|_| {
|
||||||
on_select.as_ref().map(|on_select| {
|
on_select.as_ref().map(|on_select| {
|
||||||
let character_id = character_id.clone();
|
let character_id = character_id.clone();
|
||||||
|
|
|
||||||
|
|
@ -469,10 +469,15 @@ impl Component for Host {
|
||||||
HostMessage::Echo(ServerToHostMessage::Error(GameError::NoApprenticeMentor)),
|
HostMessage::Echo(ServerToHostMessage::Error(GameError::NoApprenticeMentor)),
|
||||||
self.send.clone(),
|
self.send.clone(),
|
||||||
);
|
);
|
||||||
|
let on_prev_click = callback::send_message(
|
||||||
|
HostMessage::InGame(HostGameMessage::PreviousState),
|
||||||
|
self.send.clone(),
|
||||||
|
);
|
||||||
html! {
|
html! {
|
||||||
<nav class="debug-nav">
|
<nav class="debug-nav" style="z-index: 10;">
|
||||||
<div class="row-list">
|
<div class="row-list">
|
||||||
<Button on_click={on_error_click}>{"error"}</Button>
|
<Button on_click={on_error_click}>{"error"}</Button>
|
||||||
|
<Button on_click={on_prev_click}>{"previous"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue