apprentice gets power same night

also various cosmetic changes
This commit is contained in:
emilis 2025-11-05 19:45:29 +00:00
parent 15a6454ae2
commit 79c8c464b6
No known key found for this signature in database
15 changed files with 325 additions and 94 deletions

View File

@ -1,4 +1,8 @@
use core::{fmt::Display, num::NonZeroU8, ops::Not};
use core::{
fmt::Display,
num::NonZeroU8,
ops::{Deref, Not},
};
use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize};
@ -272,6 +276,14 @@ impl Character {
.collect())
}
/// Returns a copy of this character with their role replaced
/// in a read-only type
pub fn as_role(&self, role: Role) -> AsCharacter {
let mut char = self.clone();
char.role = role;
AsCharacter(char)
}
pub fn night_action_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
if self.mason_leader().is_ok() {
return self.mason_prompts(village);
@ -834,3 +846,13 @@ impl MasonLeaderMut<'_> {
}
}
}
pub struct AsCharacter(Character);
impl Deref for AsCharacter {
type Target = Character;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View File

@ -270,7 +270,31 @@ impl Night {
.partial_cmp(right_prompt)
.unwrap_or(core::cmp::Ordering::Equal)
});
let mut action_queue = VecDeque::from(action_queue);
let mut action_queue = VecDeque::from({
// insert actions for role-changed roles
let mut expanded_queue = Vec::new();
for action in action_queue {
match &action {
ActionPrompt::RoleChange {
character_id,
new_role,
} => {
let char = village.character_by_id(character_id.character_id)?;
let as_role = char.as_role(new_role.title_to_role_excl_apprentice());
expanded_queue.push(action);
for prompt in as_role.night_action_prompts(&village)? {
expanded_queue.push(prompt);
}
}
_ => {
expanded_queue.push(action);
continue;
}
}
}
expanded_queue
});
if night == 0 {
action_queue.push_front(ActionPrompt::WolvesIntro {
@ -716,6 +740,39 @@ impl Night {
}
}
fn received_response_consecutive_same_player_no_sleep(
&self,
resp: ActionResponse,
) -> Result<ResponseOutcome> {
let same_char = self
.current_character_id()
.and_then(|curr| {
self.action_queue
.iter()
.next()
.map(|n| n.character_id() == Some(curr))
})
.unwrap_or_default();
match (
self.received_response_consecutive_wolves_dont_sleep(resp)?,
same_char,
) {
(ResponseOutcome::PromptUpdate(p), _) => Ok(ResponseOutcome::PromptUpdate(p)),
(
ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::GoBackToSleep,
change,
}),
true,
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
result: ActionResult::Continue,
change,
})),
(act, _) => Ok(act),
}
}
fn received_response_consecutive_wolves_dont_sleep(
&self,
resp: ActionResponse,
@ -775,7 +832,7 @@ impl Night {
&self,
resp: ActionResponse,
) -> Result<BlockResolvedOutcome> {
match self.received_response_consecutive_wolves_dont_sleep(resp)? {
match self.received_response_consecutive_same_player_no_sleep(resp)? {
ResponseOutcome::PromptUpdate(update) => Ok(BlockResolvedOutcome::PromptUpdate(update)),
ResponseOutcome::ActionComplete(ActionComplete { result, change }) => {
match self
@ -864,36 +921,7 @@ impl Night {
current_prompt,
current_result: _,
..
} => match current_prompt {
ActionPrompt::Insomniac { character_id, .. }
| ActionPrompt::LoneWolfKill { character_id, .. }
| ActionPrompt::ElderReveal { character_id }
| ActionPrompt::RoleChange { character_id, .. }
| ActionPrompt::Seer { character_id, .. }
| ActionPrompt::Protector { character_id, .. }
| ActionPrompt::Arcanist { character_id, .. }
| ActionPrompt::Gravedigger { character_id, .. }
| ActionPrompt::Hunter { character_id, .. }
| ActionPrompt::Militia { character_id, .. }
| ActionPrompt::MapleWolf { character_id, .. }
| ActionPrompt::Guardian { character_id, .. }
| ActionPrompt::Shapeshifter { character_id }
| ActionPrompt::AlphaWolf { character_id, .. }
| ActionPrompt::Adjudicator { character_id, .. }
| ActionPrompt::PowerSeer { character_id, .. }
| ActionPrompt::Mortician { character_id, .. }
| ActionPrompt::Beholder { character_id, .. }
| ActionPrompt::MasonLeaderRecruit { character_id, .. }
| ActionPrompt::Empath { character_id, .. }
| ActionPrompt::Vindicator { character_id, .. }
| ActionPrompt::PyreMaster { character_id, .. }
| ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id),
ActionPrompt::WolvesIntro { wolves: _ }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::WolfPackKill { .. }
| ActionPrompt::CoverOfDarkness => None,
},
} => current_prompt.character_id(),
NightState::Complete => None,
}
}

View File

@ -13,36 +13,32 @@ use crate::{
type Result<T> = core::result::Result<T, GameError>;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, ChecksAs)]
pub enum ActionType {
Cover,
#[checks("is_wolfy")]
WolvesIntro,
RoleChange,
Protect,
#[checks("is_wolfy")]
WolfPackKill,
Direwolf,
#[checks("is_wolfy")]
Shapeshifter,
#[checks("is_wolfy")]
AlphaWolfKill,
#[checks("is_wolfy")]
OtherWolf,
#[checks("is_wolfy")]
Direwolf,
LoneWolfKill,
Block,
VillageKill,
Intel,
Other,
MasonRecruit,
MasonsWake,
Insomniac,
Beholder,
RoleChange,
}
impl ActionType {
const fn is_wolfy(&self) -> bool {
// note: Lone Wolf isn't wolfy, as they don't wake with wolves
matches!(
self,
ActionType::Direwolf
| ActionType::OtherWolf
| ActionType::WolfPackKill
| ActionType::WolvesIntro
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs, Titles)]
@ -91,7 +87,7 @@ pub enum ActionPrompt {
living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::Other)]
#[checks(ActionType::VillageKill)]
Militia {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
@ -159,7 +155,7 @@ pub enum ActionPrompt {
living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::Other)]
#[checks(ActionType::VillageKill)]
PyreMaster {
character_id: CharacterIdentity,
living_players: Box<[CharacterIdentity]>,
@ -171,9 +167,9 @@ pub enum ActionPrompt {
living_villagers: Box<[CharacterIdentity]>,
marked: Option<CharacterId>,
},
#[checks(ActionType::OtherWolf)]
#[checks(ActionType::Shapeshifter)]
Shapeshifter { character_id: CharacterIdentity },
#[checks(ActionType::OtherWolf)]
#[checks(ActionType::AlphaWolfKill)]
AlphaWolf {
character_id: CharacterIdentity,
living_villagers: Box<[CharacterIdentity]>,
@ -196,6 +192,38 @@ pub enum ActionPrompt {
}
impl ActionPrompt {
pub(crate) const fn character_id(&self) -> Option<CharacterId> {
match self {
ActionPrompt::Insomniac { character_id, .. }
| ActionPrompt::LoneWolfKill { character_id, .. }
| ActionPrompt::ElderReveal { character_id }
| ActionPrompt::RoleChange { character_id, .. }
| ActionPrompt::Seer { character_id, .. }
| ActionPrompt::Protector { character_id, .. }
| ActionPrompt::Arcanist { character_id, .. }
| ActionPrompt::Gravedigger { character_id, .. }
| ActionPrompt::Hunter { character_id, .. }
| ActionPrompt::Militia { character_id, .. }
| ActionPrompt::MapleWolf { character_id, .. }
| ActionPrompt::Guardian { character_id, .. }
| ActionPrompt::Shapeshifter { character_id }
| ActionPrompt::AlphaWolf { character_id, .. }
| ActionPrompt::Adjudicator { character_id, .. }
| ActionPrompt::PowerSeer { character_id, .. }
| ActionPrompt::Mortician { character_id, .. }
| ActionPrompt::Beholder { character_id, .. }
| ActionPrompt::MasonLeaderRecruit { character_id, .. }
| ActionPrompt::Empath { character_id, .. }
| ActionPrompt::Vindicator { character_id, .. }
| ActionPrompt::PyreMaster { character_id, .. }
| ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id),
ActionPrompt::WolvesIntro { .. }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::WolfPackKill { .. }
| ActionPrompt::CoverOfDarkness => None,
}
}
pub(crate) fn matches_beholding(&self, target: CharacterId) -> bool {
match self {
ActionPrompt::Insomniac { character_id, .. }

View File

@ -219,7 +219,6 @@ pub enum Role {
#[checks(Alignment::Village)]
#[checks(Powerful::Powerful)]
#[checks(Killer::NotKiller)]
#[checks("is_mentor")]
Elder {
knows_on_night: NonZeroU8,
woken_for_reveal: bool,

28
werewolves/img/equal.svg Normal file
View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="81.560997mm"
height="48.303188mm"
viewBox="0 0 81.560997 48.303188"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer4"
transform="translate(-50.121599,-612.75354)"><g
id="g9"><rect
style="fill:#3c34ff;fill-opacity:1;stroke:#0f07ff;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
id="rect6"
width="80"
height="20"
x="50.902103"
y="613.53406" /><rect
style="fill:#3c34ff;fill-opacity:1;stroke:#0f07ff;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
id="rect6-1"
width="80"
height="20"
x="50.902103"
y="640.27625" /></g></g></svg>

After

Width:  |  Height:  |  Size: 932 B

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="81.560997mm"
height="79.98938mm"
viewBox="0 0 81.560997 79.98938"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer4"
transform="translate(-50.121599,-596.91049)"><g
id="g9"><rect
style="fill:#3c34ff;fill-opacity:1;stroke:#0f07ff;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
id="rect6"
width="80"
height="20"
x="50.902103"
y="613.53406" /><rect
style="fill:#3c34ff;fill-opacity:1;stroke:#0f07ff;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
id="rect6-1"
width="80"
height="20"
x="50.902103"
y="640.27625" /></g><rect
style="fill:#ff0707;fill-opacity:1;stroke:#c10000;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
id="rect6-3"
width="100"
height="10"
x="-436.08246"
y="509.63745"
transform="rotate(-45)" /></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

19
werewolves/img/red-x.svg Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="87.060303mm"
height="87.060318mm"
viewBox="0 0 87.060303 87.060318"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer4"
transform="translate(-207.43333,-614.36255)"><path
id="rect6-3-3"
style="fill:#ff0707;fill-opacity:1;stroke:#c10000;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
d="m -277.74226,592.65854 h -20.00022 v 39.9997 h -39.99971 v 20.00022 h 39.99971 v 39.9997 h 20.00022 v -39.9997 h 39.9997 v -20.00022 h -39.9997 z"
transform="rotate(-45)" /></g></svg>

After

Width:  |  Height:  |  Size: 776 B

View File

@ -102,7 +102,7 @@ app {
left: 0;
top: 0;
margin: 0;
font-size: 2rem;
font-size: 2.7vw;
}
$link_color: #432054;
@ -383,16 +383,38 @@ button {
}
.identity {
font-size: 1.5em;
font-size: 1.5rem;
}
}
.day-char {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
// min-width: 1vw;
align-items: center;
}
.red {
color: red;
}
.character {
text-align: center;
border: 3px solid rgba(0, 0, 0, 0.4);
// min-width: 20%;
flex-shrink: 1;
.headline {
display: flex;
flex-direction: column;
align-items: center;
flex-wrap: nowrap;
overflow: hidden;
gap: 5px;
}
.role {
font-size: 1.5rem;
@ -881,6 +903,7 @@ error {
.binary {
margin: 0;
padding: 0;
font-size: 1.5vw;
.button-container {
text-align: center;
@ -1281,7 +1304,7 @@ input {
.setup-screen {
margin-top: 2%;
font-size: 1rem;
font-size: 1.5vw;
.setup {
display: flex;
@ -1341,6 +1364,7 @@ input {
filter: saturate(40%);
padding-left: 10px;
padding-right: 10px;
font-weight: bolder;
&.wakes {
border: 2px solid yellow;
@ -1361,7 +1385,8 @@ input {
}
.inactive {
filter: grayscale(100%) brightness(30%);
// filter: grayscale(100%) brightness(30%);
filter: brightness(0%);
}
.qrcode {
@ -1383,6 +1408,7 @@ input {
}
.details {
font-size: 5vw;
// height: 100%;
// width: 100%;
border: 1px solid $village_border;
@ -1485,12 +1511,17 @@ input {
font-size: 1.5rem;
}
&.full-height {
height: 100vh;
}
& input {
height: 2rem;
text-align: center;
&#number {
#number {
font-size: 2rem;
max-width: 50vw;
}
}
}
@ -1724,10 +1755,11 @@ input {
h1 {
text-align: center;
// align-self: flex-start;
font-size: 4vw;
}
.information {
font-size: 1.2em;
font-size: 1.2rem;
padding-left: 5%;
padding-right: 5%;
}
@ -1764,7 +1796,7 @@ input {
gap: 10px;
&.masons {
font-size: 2em;
font-size: 2rem;
}
}

View File

@ -459,13 +459,17 @@ impl Component for Host {
<Button on_click={to_normal}>{"small screen"}</Button>
}
} else {
let to_big = Callback::from(|_| {
if let Some(loc) = gloo::utils::document().location() {
let _ = loc.replace("/host/big");
}
});
// let to_big = Callback::from(|_| {
// if let Some(loc) = gloo::utils::document().location() {
// let _ = loc.replace("/host/big");
// }
// });
html! {
<Button on_click={to_big}>{"big screen 📺"}</Button>
<a href="/host/big">
<Button on_click={Callback::default()}>
{"big screen 📺"}
</Button>
</a>
}
};
let story_on_click = if let HostState::Story { .. } = &self.state {

View File

@ -47,7 +47,7 @@ pub fn Signin(props: &SigninProps) -> Html {
let on_change = crate::components::input_element_number_oninput(num_value);
html! {
<div class="signin">
<div class="signin full-height">
<div class="column-list">
<label for="name">{"Name"}</label>
<input oninput={name_on_input} name="name" id="name" type="text"/>

View File

@ -1,5 +1,6 @@
use core::{num::NonZeroU8, ops::Not};
use convert_case::{Case, Casing};
use werewolves_proto::{
character::CharacterId,
game::GameTime,
@ -7,7 +8,10 @@ use werewolves_proto::{
};
use yew::prelude::*;
use crate::components::{Button, Identity};
use crate::components::{
AssociatedIcon, Button, Icon, IconType, Identity, PartialAssociatedIcon,
attributes::RoleTitleSpan,
};
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct DaytimePlayerListProps {
@ -115,7 +119,7 @@ pub fn DaytimePlayer(
character:
CharacterState {
player_id: _,
role: _,
role,
died_to,
identity,
},
@ -133,9 +137,18 @@ pub fn DaytimePlayer(
)
.unwrap_or_default();
let identity: PublicIdentity = identity.into();
let icon = role.icon().unwrap_or_else(|| role.alignment().icon());
let text = role.to_string().to_case(Case::Title);
let align_class = role.wolf().then_some("red");
html! {
<Button on_click={on_click} classes={classes!(class, "character")}>
<Identity ident={identity}/>
<div class="day-char">
<span class={classes!("headline")}>
<Icon source={icon} icon_type={IconType::Small}/>
<span class={classes!(align_class)}>{text}</span>
</span>
<Identity ident={identity}/>
</div>
</Button>
}
}

View File

@ -6,6 +6,7 @@ use yew::prelude::*;
macro_rules! decl_icon {
($($name:ident: $path:literal,)*) => {
#[allow(unused)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IconSource {
$(
@ -58,6 +59,9 @@ decl_icon!(
Insomniac: "/img/insomniac.svg",
BlackKnight: "/img/black-knight.svg",
Mason: "/img/mason.svg",
NotEqual: "/img/not-equal.svg",
Equal: "/img/equal.svg",
RedX: "/img/red-x.svg",
);
impl IconSource {

View File

@ -33,16 +33,26 @@ pub fn AdjudicatorResult(AdjudicatorResultProps { killer }: &AdjudicatorResultPr
Killer::Killer => "IS A KILLER",
Killer::NotKiller => "IS NOT A KILLER",
};
let icon = match killer {
Killer::Killer => html! {
<Icon
source={IconSource::Killer}
icon_type={IconType::Informational}
/>
},
Killer::NotKiller => html! {
<Icon
source={IconSource::RedX}
icon_type={IconType::Informational}
/>
},
};
html! {
<div class="role-page">
<h1 class="defensive">{"ADJUDICATOR"}</h1>
<div class="information defensive faint">
<h2>{"YOUR TARGET"}</h2>
<h4><Icon
source={IconSource::Killer}
icon_type={IconType::Informational}
inactive={killer.killer().not()}
/></h4>
<h4>{icon}</h4>
<h3 class="yellow">{text}</h3>
</div>
</div>

View File

@ -12,8 +12,8 @@ pub fn ArcanistPage1() -> Html {
<h2>{"PICK TWO PLAYERS"}</h2>
<h4 class="icons">
<Icon source={IconSource::Village} icon_type={IconType::Informational}/>
<Icon source={IconSource::Village} icon_type={IconType::Informational}/>
<Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
// <Icon source={IconSource::Village} icon_type={IconType::Informational}/>
// <Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
<Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
</h4>
<h3 class="yellow">{"YOU WILL CHECK IF THEIR ALIGNMENTS ARE THE SAME OR DIFFERENT"}</h3>
@ -36,20 +36,21 @@ pub fn ArcanistResult(ArcanistResultProps { alignment_eq }: &ArcanistResultProps
let icons = match alignment_eq {
AlignmentEq::Same => html! {
<>
<Icon source={IconSource::Village} icon_type={IconType::Informational}/>
<Icon source={IconSource::Village} icon_type={IconType::Informational}/>
<span>{"OR"}</span>
<Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
<Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
// <Icon source={IconSource::Village} icon_type={IconType::Informational}/>
// <Icon source={IconSource::Village} icon_type={IconType::Informational}/>
// <span>{"OR"}</span>
// <Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
// <Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
<Icon source={IconSource::Equal} icon_type={IconType::Informational} />
</>
},
AlignmentEq::Different => html! {
<>
<Icon source={IconSource::Village} icon_type={IconType::Informational}/>
<Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
{"OR"}
<Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
<Icon source={IconSource::Village} icon_type={IconType::Informational}/>
// {"OR"}
// <Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
// <Icon source={IconSource::Village} icon_type={IconType::Informational}/>
</>
},
};
@ -61,7 +62,7 @@ pub fn ArcanistResult(ArcanistResultProps { alignment_eq }: &ArcanistResultProps
<h4 class="icons">
{icons}
</h4>
<h3 class="yellow">{text}</h3>
<h1 class="yellow">{text}</h1>
</div>
</div>
}

View File

@ -33,18 +33,26 @@ pub fn PowerSeerResult(PowerSeerResultProps { powerful }: &PowerSeerResultProps)
Powerful::Powerful => "POWERFUL",
Powerful::NotPowerful => "NOT POWERFUL",
};
let icon = match powerful {
Powerful::Powerful => html! {
<Icon
source={IconSource::Powerful}
icon_type={IconType::Informational}
/>
},
Powerful::NotPowerful => html! {
<Icon
source={IconSource::RedX}
icon_type={IconType::Informational}
/>
},
};
html! {
<div class="role-page">
<h1 class="intel">{"POWER SEER"}</h1>
<div class="information intel faint">
<h2>{"YOUR TARGET APPEARS AS"}</h2>
<h4>
<Icon
source={powerful.icon()}
icon_type={IconType::Informational}
inactive={powerful.powerful().not()}
/>
</h4>
<h4>{icon}</h4>
<h3 class="yellow">{text}</h3>
</div>
</div>