binary choice for shapeshift, added better shapeshift handling

This commit is contained in:
emilis 2025-09-27 00:14:26 +01:00
parent 09039c21c1
commit e704fdca8b
No known key found for this signature in database
7 changed files with 133 additions and 14 deletions

View File

@ -343,6 +343,57 @@ impl Night {
Ok(new_village) Ok(new_village)
} }
fn apply_shapeshift(&mut self, source: &CharacterId) -> Result<ActionResult> {
if let Some(kill_target) = self.changes.iter().find_map(|c| match c {
NightChange::Kill {
target,
died_to: DiedTo::Wolfpack { night: _ },
} => Some(target.clone()),
_ => None,
}) {
if self.changes.iter().any(|c| match c {
NightChange::Protection {
target,
protection: _,
} => target == &kill_target,
_ => false,
}) {
// 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| {
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(),
},
}
}
self.changes.push(NightChange::Shapeshift {
source: source.clone(),
});
self.action_queue.push_front((
ActionPrompt::RoleChange {
new_role: RoleTitle::Werewolf,
},
self.village
.find_by_character_id(&kill_target)
.unwrap()
.clone(),
));
}
Ok(ActionResult::GoBackToSleep)
}
pub fn received_response(&mut self, resp: ActionResponse) -> Result<ActionResult> { pub fn received_response(&mut self, resp: ActionResponse) -> Result<ActionResult> {
if let ( if let (
NightState::Active { NightState::Active {
@ -367,6 +418,10 @@ impl Night {
} => current_result.replace(result.clone()), } => current_result.replace(result.clone()),
NightState::Complete => return Err(GameError::NightOver), NightState::Complete => return Err(GameError::NightOver),
}; };
if let NightChange::Shapeshift { source } = &change {
// needs to be resolved _now_
return self.apply_shapeshift(source);
}
self.changes.push(change); self.changes.push(change);
Ok(result) Ok(result)
} }

View File

@ -118,10 +118,7 @@ pub enum ActionResult {
Seer(Alignment), Seer(Alignment),
Arcanist { same: bool }, Arcanist { same: bool },
GraveDigger(Option<RoleTitle>), GraveDigger(Option<RoleTitle>),
WolvesMustBeUnanimous,
WaitForOthersToVote,
GoBackToSleep, GoBackToSleep,
RoleRevealDone,
WolvesIntroDone, WolvesIntroDone,
} }

View File

@ -36,6 +36,9 @@ impl CharacterId {
pub fn new() -> Self { pub fn new() -> Self {
Self(uuid::Uuid::new_v4()) Self(uuid::Uuid::new_v4())
} }
pub const fn from_u128(v: u128) -> Self {
Self(uuid::Uuid::from_u128(v))
}
} }
impl Display for CharacterId { impl Display for CharacterId {

View File

@ -0,0 +1,32 @@
use yew::prelude::*;
use crate::components::Button;
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct BinaryChoiceProps {
pub on_chosen: Callback<bool>,
#[prop_or_default]
pub children: Html,
}
#[function_component]
pub fn BinaryChoice(
BinaryChoiceProps {
on_chosen,
children,
}: &BinaryChoiceProps,
) -> Html {
let on_chosen_yes = on_chosen.clone();
let yes = move |_| on_chosen_yes.emit(true);
let on_chosen = on_chosen.clone();
let no = move |_| on_chosen.emit(false);
html! {
<div class="column-list">
{children.clone()}
<div class="row-list">
<Button on_click={yes}>{"yes"}</Button>
<Button on_click={no}>{"no"}</Button>
</div>
</div>
}
}

View File

@ -1,4 +1,4 @@
use core::ops::Not; use core::{num::NonZeroU8, ops::Not};
use werewolves_proto::{ use werewolves_proto::{
message::{ message::{
@ -12,7 +12,7 @@ use yew::prelude::*;
use crate::components::{ use crate::components::{
Identity, Identity,
action::{SingleTarget, TargetSelection, WolvesIntro}, action::{BinaryChoice, SingleTarget, TargetSelection, WolvesIntro},
}; };
#[derive(Debug, Clone, PartialEq, Properties)] #[derive(Debug, Clone, PartialEq, Properties)]
@ -162,7 +162,19 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
</div> </div>
} }
} }
ActionPrompt::Shapeshifter => todo!(), ActionPrompt::Shapeshifter => {
let on_complete = props.on_complete.clone();
let on_select = move |shift| {
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
HostNightMessage::ActionResponse(ActionResponse::Shapeshifter(shift)),
)));
};
html! {
<BinaryChoice on_chosen={on_select}>
<h2>{"shapeshift?"}</h2>
</BinaryChoice>
}
}
ActionPrompt::AlphaWolf { living_villagers } => { ActionPrompt::AlphaWolf { 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(|| {

View File

@ -1,10 +1,11 @@
use core::ops::Not; use core::ops::Not;
use convert_case::{Case, Casing};
use werewolves_proto::{ use werewolves_proto::{
message::{ message::{
PublicIdentity, PublicIdentity,
host::{HostGameMessage, HostMessage, HostNightMessage}, host::{HostGameMessage, HostMessage, HostNightMessage},
night::{ActionPrompt, ActionResponse, ActionResult}, night::ActionResult,
}, },
role::Alignment, role::Alignment,
}; };
@ -58,10 +59,30 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
{cont} {cont}
</div> </div>
}, },
ActionResult::Arcanist { same } => todo!(), ActionResult::Arcanist { same } => {
ActionResult::GraveDigger(role_title) => todo!(), let outcome = if *same { "same" } else { "different" };
ActionResult::WolvesMustBeUnanimous => todo!(), html! {
ActionResult::WaitForOthersToVote => todo!(), <div class="result">
{ident}
<h2>{"the alignments are:"}</h2>
<p>{outcome}</p>
{cont}
</div>
}
}
ActionResult::GraveDigger(role_title) => {
let dig = role_title
.map(|r| r.to_string().to_case(Case::Title))
.unwrap_or_else(|| String::from("an empty grave"));
html! {
<div class="result">
{ident}
<h2>{"you see:"}</h2>
<p>{dig}</p>
{cont}
</div>
}
}
ActionResult::GoBackToSleep => { ActionResult::GoBackToSleep => {
let next = props.big_screen.not().then(|| { let next = props.big_screen.not().then(|| {
let on_complete = props.on_complete.clone(); let on_complete = props.on_complete.clone();
@ -77,7 +98,6 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
</CoverOfDarkness> </CoverOfDarkness>
} }
} }
ActionResult::RoleRevealDone => todo!(),
ActionResult::WolvesIntroDone => { ActionResult::WolvesIntroDone => {
let on_complete = props.on_complete.clone(); let on_complete = props.on_complete.clone();
let next = props.big_screen.not().then(|| { let next = props.big_screen.not().then(|| {

View File

@ -1,4 +1,4 @@
use core::{fmt::Debug, marker::PhantomData, ops::Not}; use core::fmt::Debug;
use werewolves_proto::{message::Target, player::CharacterId}; use werewolves_proto::{message::Target, player::CharacterId};
use yew::prelude::*; use yew::prelude::*;
@ -93,7 +93,7 @@ impl Component for SingleTarget {
if self.selected.len() > 1 { if self.selected.len() > 1 {
None None
} else { } else {
let selected = self.selected.iter().next().cloned(); let selected = self.selected.first().cloned();
let on_click = on_click.clone(); let on_click = on_click.clone();
Some(Callback::from(move |_| on_click.emit(selected.clone()))) Some(Callback::from(move |_| on_click.emit(selected.clone())))
} }