big screen setup display

This commit is contained in:
emilis 2025-10-06 01:03:16 +01:00
parent c0838c276c
commit 0889acca6a
No known key found for this signature in database
17 changed files with 984 additions and 115 deletions

View File

@ -0,0 +1,47 @@
use quote::{ToTokens, quote};
use syn::spanned::Spanned;
pub struct All {
name: syn::Ident,
variants: Box<[syn::Ident]>,
}
impl All {
pub fn parse(input: syn::DeriveInput) -> Result<Self, syn::Error> {
let data = match input.data {
syn::Data::Enum(enu) => enu,
_ => {
return Err(syn::Error::new(
input.span(),
"All can only be used on enums",
));
}
};
let variants = data
.variants
.into_iter()
.map(|v| match &v.fields {
syn::Fields::Named(_) | syn::Fields::Unnamed(_) => Err(syn::Error::new(
v.ident.span(),
"All can only be used on enums with only unit fields",
)),
syn::Fields::Unit => Ok(v.ident),
})
.collect::<Result<Box<[_]>, _>>()?;
let name = input.ident;
Ok(Self { name, variants })
}
}
impl ToTokens for All {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let name = &self.name;
let variants = &self.variants;
let count = self.variants.len();
tokens.extend(quote! {
impl #name {
pub const ALL: [#name; #count] = [#(#name::#variants),*];
}
});
}
}

View File

@ -9,6 +9,7 @@ use proc_macro2::Span;
use quote::{ToTokens, quote};
use syn::{parse::Parse, parse_macro_input};
mod all;
mod checks;
pub(crate) mod hashlist;
mod targets;
@ -163,9 +164,10 @@ where
continue;
}
if let Some(file_name) = item.file_name().to_str()
&& include_in_rerun(file_name) {
out.push(FileWithPath::from_path(item.path(), origin_path)?);
}
&& include_in_rerun(file_name)
{
out.push(FileWithPath::from_path(item.path(), origin_path)?);
}
}
Ok(())
@ -542,3 +544,10 @@ pub fn extract(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
quote! {#checks_as}.into()
}
#[proc_macro_derive(All)]
pub fn all(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let all = all::All::parse(parse_macro_input!(input)).unwrap();
quote! {#all}.into()
}

View File

@ -23,7 +23,7 @@ use crate::{
};
pub use {
settings::{Category, GameSettings, OrRandom, SetupRole, SetupSlot, SlotId},
settings::{Category, GameSettings, OrRandom, SetupRole, SetupRoleTitle, SetupSlot, SlotId},
village::Village,
};

View File

@ -209,11 +209,11 @@ impl GameSettings {
.filter(|r| Into::<RoleTitle>::into(r.role.clone()).is_mentor())
.count();
self.roles.iter().try_for_each(|s| match &s.role {
SetupRole::Apprentice { specifically: None } => (mentor_count > 0)
SetupRole::Apprentice { to: None } => (mentor_count > 0)
.then_some(())
.ok_or(GameError::NoApprenticeMentor),
SetupRole::Apprentice {
specifically: Some(role),
to: Some(role),
} => role
.is_mentor()
.then_some(())

View File

@ -6,7 +6,7 @@ use core::{
use rand::distr::{Distribution, StandardUniform};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use werewolves_macros::ChecksAs;
use werewolves_macros::{All, ChecksAs, Titles};
use crate::{
error::GameError,
@ -40,7 +40,9 @@ where
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, ChecksAs)]
#[derive(
Debug, PartialOrd, Ord, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ChecksAs, All,
)]
pub enum Category {
#[checks]
Wolves,
@ -50,8 +52,30 @@ pub enum Category {
Offensive,
StartsAsVillager,
}
impl Category {
pub fn entire_category(&self) -> Box<[SetupRoleTitle]> {
SetupRoleTitle::ALL
.iter()
.filter(|r| r.category() == *self)
.cloned()
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ChecksAs)]
impl Display for Category {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Category::Wolves => "Wolves",
Category::Villager => "Villager",
Category::Intel => "Intel",
Category::Defensive => "Defensive",
Category::Offensive => "Offensive",
Category::StartsAsVillager => "Starts As Villager",
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ChecksAs, Titles)]
pub enum SetupRole {
#[checks(Category::Villager)]
Villager,
@ -74,7 +98,7 @@ pub enum SetupRole {
#[checks(Category::Defensive)]
Protector,
#[checks(Category::StartsAsVillager)]
Apprentice { specifically: Option<RoleTitle> },
Apprentice { to: Option<RoleTitle> },
#[checks(Category::StartsAsVillager)]
Elder { knows_on_night: NonZeroU8 },
@ -88,6 +112,39 @@ pub enum SetupRole {
Shapeshifter,
}
impl SetupRoleTitle {
pub const fn into_role(self) -> Role {
match self {
SetupRoleTitle::Villager => Role::Villager,
SetupRoleTitle::Scapegoat => Role::Scapegoat { redeemed: false },
SetupRoleTitle::Seer => Role::Seer,
SetupRoleTitle::Arcanist => Role::Arcanist,
SetupRoleTitle::Gravedigger => Role::Gravedigger,
SetupRoleTitle::Hunter => Role::Hunter { target: None },
SetupRoleTitle::Militia => Role::Militia { targeted: None },
SetupRoleTitle::MapleWolf => Role::MapleWolf {
last_kill_on_night: 0,
},
SetupRoleTitle::Guardian => Role::Guardian {
last_protected: None,
},
SetupRoleTitle::Protector => Role::Protector {
last_protected: None,
},
SetupRoleTitle::Apprentice => Role::Apprentice(RoleTitle::Arcanist),
SetupRoleTitle::Elder => Role::Elder {
woken_for_reveal: false,
lost_protection_night: None,
knows_on_night: NonZeroU8::new(1).unwrap(),
},
SetupRoleTitle::Werewolf => Role::Werewolf,
SetupRoleTitle::AlphaWolf => Role::AlphaWolf { killed: None },
SetupRoleTitle::DireWolf => Role::DireWolf,
SetupRoleTitle::Shapeshifter => Role::Shapeshifter { shifted_into: None },
}
}
}
impl Display for SetupRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
@ -132,10 +189,8 @@ impl SetupRole {
SetupRole::Protector => Role::Protector {
last_protected: None,
},
SetupRole::Apprentice {
specifically: Some(role),
} => Role::Apprentice(role),
SetupRole::Apprentice { specifically: None } => {
SetupRole::Apprentice { to: Some(role) } => Role::Apprentice(role),
SetupRole::Apprentice { to: None } => {
let mentors = roles_in_game
.iter()
.filter(|r| r.is_mentor())
@ -197,7 +252,7 @@ impl From<RoleTitle> for SetupRole {
RoleTitle::MapleWolf => SetupRole::MapleWolf,
RoleTitle::Guardian => SetupRole::Guardian,
RoleTitle::Protector => SetupRole::Protector,
RoleTitle::Apprentice => SetupRole::Apprentice { specifically: None },
RoleTitle::Apprentice => SetupRole::Apprentice { to: None },
RoleTitle::Elder => SetupRole::Elder {
knows_on_night: NonZeroU8::new(3).unwrap(),
},

View File

@ -99,29 +99,33 @@ impl Role {
}
}
pub const fn wakes_night_zero(&self) -> bool {
match self {
Role::DireWolf | Role::Arcanist | Role::Seer => true,
Role::Shapeshifter { .. }
| Role::Werewolf
| Role::AlphaWolf { .. }
| Role::Elder { .. }
| Role::Gravedigger
| Role::Hunter { .. }
| Role::Militia { .. }
| Role::MapleWolf { .. }
| Role::Guardian { .. }
| Role::Apprentice(_)
| Role::Villager
| Role::Scapegoat { .. }
| Role::Protector { .. } => false,
}
}
pub fn wakes(&self, village: &Village) -> bool {
let night_zero = match village.date_time() {
DateTime::Day { number: _ } => return false,
DateTime::Night { number } => number == 0,
};
if night_zero {
return match self {
Role::DireWolf | Role::Arcanist | Role::Seer => true,
Role::Shapeshifter { .. }
| Role::Werewolf
| Role::AlphaWolf { .. }
| Role::Elder { .. }
| Role::Gravedigger
| Role::Hunter { .. }
| Role::Militia { .. }
| Role::MapleWolf { .. }
| Role::Guardian { .. }
| Role::Apprentice(_)
| Role::Villager
| Role::Scapegoat { .. }
| Role::Protector { .. } => false,
};
return self.wakes_night_zero();
}
match self {
Role::AlphaWolf { killed: Some(_) }

View File

@ -237,7 +237,7 @@ impl Client {
self.handle_message(msg).await
}
Err(err) => {
log::warn!("[{}] recv error: {err}", self.connection_id.player_id());
log::debug!("[{}] recv error: {err}", self.connection_id.player_id());
return;
}
}

View File

@ -86,12 +86,13 @@ impl Lobby {
}
pub async fn next(&mut self) -> Option<GameRunner> {
let msg = self
.comms()
.unwrap()
.next_message()
.await
.expect("get next message"); // TODO: keeps happening
let msg = match self.comms().unwrap().next_message().await {
Ok(msg) => msg,
Err(err) => {
log::error!("get next message: {err}");
return None;
}
};
match self.next_inner(msg.clone()).await.map_err(|err| (msg, err)) {
Ok(None) => {}
@ -162,7 +163,14 @@ impl Lobby {
))
.log_warn(),
Message::Host(HostMessage::Lobby(HostLobbyMessage::GetState))
| Message::Host(HostMessage::GetState) => self.send_lobby_info_to_host().await?,
| Message::Host(HostMessage::GetState) => {
self.send_lobby_info_to_host().await?;
let settings = self.settings.clone();
self.comms()?
.host()
.send(ServerToHostMessage::GameSettings(settings))
.log_warn();
}
Message::Host(HostMessage::Lobby(HostLobbyMessage::GetGameSettings)) => {
let msg = ServerToHostMessage::GameSettings(self.settings.clone());
let _ = self.comms().unwrap().host().send(msg);

94
werewolves/img/killer.svg Normal file
View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="52.665638mm"
height="52.665649mm"
viewBox="0 0 52.665638 52.665649"
version="1.1"
id="svg1"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true">
<inkscape:page
x="0"
y="0"
width="52.665638"
height="52.665649"
id="page2"
margin="0"
bleed="0" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-53.30057,-132.77008)">
<path
sodipodi:type="star"
style="fill:#d45500;stroke-width:2.348"
id="path4-9"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="51.531715"
sodipodi:cy="30.475746"
sodipodi:r1="37.207153"
sodipodi:r2="18.603577"
sodipodi:arg1="-1.5707963"
sodipodi:arg2="-0.52359875"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 51.531716,-6.7314072 32.222339,55.8107312 -64.44468,-2e-6 z"
inkscape:transform-center-x="-0.11920648"
inkscape:transform-center-y="-9.0503731"
transform="matrix(0.2916535,-0.2916535,0.70710678,0.70710678,43.030992,152.55931)" />
<path
sodipodi:type="star"
style="fill:#ffccaa;fill-opacity:1;stroke-width:2.348"
id="path4-1-2"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="51.531715"
sodipodi:cy="30.475746"
sodipodi:r1="37.207153"
sodipodi:r2="18.603577"
sodipodi:arg1="-1.5707963"
sodipodi:arg2="-0.52359875"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 51.531716,-6.7314072 32.222339,55.8107312 -64.44468,-2e-6 z"
inkscape:transform-center-x="-0.11920648"
inkscape:transform-center-y="-9.0503731"
transform="matrix(0.14582674,-0.14582674,0.35355339,0.35355339,67.897856,162.39677)" />
<rect
style="fill:#ffccaa;fill-opacity:1;stroke-width:2.28741"
id="rect4-2"
width="34.461983"
height="3.9066164"
x="-73.424423"
y="183.5461"
transform="rotate(-45)" />
<rect
style="fill:#ffccaa;fill-opacity:1;stroke-width:1.79377"
id="rect5-8"
width="6.5479999"
height="15.395733"
x="-59.46743"
y="187.38255"
transform="rotate(-45)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="52.348007mm"
height="52.348mm"
viewBox="0 0 52.348007 52.348"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="icons.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="1.4142136"
inkscape:cx="346.12877"
inkscape:cy="548.36131"
inkscape:window-width="1918"
inkscape:window-height="1042"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="0"
inkscape:current-layer="layer1">
<inkscape:page
x="0"
y="0"
width="52.348007"
height="52.348"
id="page2"
margin="0"
bleed="0" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
transform="translate(-119.56806,-115.26503)">
<circle
style="fill:#54ffff;fill-opacity:0.146665;stroke:#00adc1;stroke-width:2.348;stroke-opacity:1"
id="path9"
cx="145.74207"
cy="141.43904"
r="25"
inkscape:export-filename="../../src/werewolves/werewolves/img/powerful.svg"
inkscape:export-xdpi="900.08"
inkscape:export-ydpi="900.08" />
<path
style="fill:#00adc1;fill-opacity:1;stroke:#00adc1;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 146.7354,116.77348 -12.81832,29.07761 11.35727,-0.99768 -9.76266,20.13334 23.61744,-27.67112 -15.11625,2.07403 12.86097,-20.49482 z"
id="path10"
sodipodi:nodetypes="cccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="52.348mm"
height="52.348mm"
viewBox="0 0 52.348 52.348"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="icons.svg"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="true"
inkscape:zoom="1"
inkscape:cx="71.5"
inkscape:cy="601.5"
inkscape:window-width="1918"
inkscape:window-height="1042"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="0"
inkscape:current-layer="layer3"><inkscape:page
x="0"
y="0"
width="52.348"
height="52.348"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1" /><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 4"
transform="translate(-31.749998,-194.46876)"><circle
style="fill:#0000ff;fill-opacity:0.15;stroke:#0f07ff;stroke-width:2.348;stroke-opacity:1"
id="path12-8"
cx="57.924"
cy="220.64275"
r="25"
inkscape:export-filename="../../src/werewolves/werewolves/img/wolf.svg"
inkscape:export-xdpi="900.08"
inkscape:export-ydpi="900.08" /><path
id="path52"
style="fill:#0f07ff;fill-opacity:1;stroke:#0f07ff;stroke-width:0.646547;stroke-dasharray:none;stroke-opacity:1"
inkscape:transform-center-x="0.38117126"
inkscape:transform-center-y="-3.2844551"
d="m 57.107952,201.21922 c -1.713539,0.0921 -3.402067,0.64565 -4.275191,1.61799 -1.746251,1.94469 -1.477016,6.95297 0.467671,8.69922 0.577742,0.51878 1.869738,0.91451 2.822455,1.08642 0.645263,0.48645 0.441435,1.16866 0.235128,1.54824 -2.113413,0.40992 -4.423609,1.57646 -5.591796,3.31071 -3.059556,4.54209 -0.05893,15.93547 0,21.41161 0.01685,1.56554 14.736774,1.63168 14.555185,-0.0661 -0.582408,-5.44541 2.927265,-16.80337 -0.132292,-21.34547 -1.319465,-1.95883 -3.263349,-3.04974 -5.692251,-3.31904 -0.233696,-0.43409 -0.260906,-1.13472 0.150896,-1.67225 1.115403,-0.2586 1.751648,-0.75274 2.352402,-1.42176 1.746251,-1.94469 1.477016,-6.95348 -0.467672,-8.69973 -0.972343,-0.87312 -2.710997,-1.24193 -4.424535,-1.1498 z"
sodipodi:nodetypes="sssccssssccssss" /><path
id="rect55"
style="display:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.7;stroke-dasharray:none;stroke-opacity:1"
d="m 54.680079,205.91192 0.0075,3.95139 h -0.02186 v 1.21435 h 2.712393 v 25.2552 l 0.5302,0.68461 0.58136,-0.68461 v -25.2552 h 2.712392 v -1.21435 h -0.02232 l -0.007,-3.95139 -0.404006,-1.44994 -0.408967,1.44994 0.007,3.95139 h -1.877095 -0.149294 l -0.0075,-3.95139 -0.440632,-1.43824 -0.372342,1.43824 0.007,3.95139 H 57.37808 55.50052 l -0.007,-3.95139 -0.442679,-1.43824 z"
sodipodi:nodetypes="ccccccccccccccccccccccccccc" /></g></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

220
werewolves/img/wolf.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -1078,3 +1078,94 @@ input {
font-size: 2em;
}
.top-settings {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
}
.setup-screen {
width: 80%;
height: 80%;
position: fixed;
left: 10%;
top: 10%;
.setup {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 5%;
width: 100%;
height: 80%;
}
.category {
margin-bottom: 30px;
width: 30%;
text-align: center;
display: flex;
flex-direction: column;
& .title {
margin-bottom: 10px;
}
& .count {
left: -30px;
position: relative;
width: 0;
height: 0;
}
.category-list {
text-align: left;
flex: 1, 1, 100%;
display: flex;
flex-wrap: nowrap;
flex-direction: column;
gap: 5px;
.slot {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
.attributes {
margin-left: 10px;
align-self: flex-end;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 10px;
.icon {
width: 32px;
height: 32px;
&:hover {
filter: contrast(120%) brightness(120%);
}
}
.inactive {
filter: grayscale(100%) brightness(30%);
}
}
.role {
width: 100%;
filter: saturate(40%);
padding-left: 10px;
padding-right: 10px;
&.wakes {
border: 2px solid yellow;
}
}
}
}
}
}

View File

@ -28,7 +28,7 @@ use crate::{
components::{
Button, CoverOfDarkness, Lobby, LobbyPlayerAction, RoleReveal, Settings,
action::{ActionResultView, Prompt},
host::DaytimePlayerList,
host::{DaytimePlayerList, Setup},
},
};
@ -302,76 +302,10 @@ impl Component for Host {
</div>
},
HostState::Lobby { players, settings } => {
let on_error = self.error_callback.clone();
let settings = self.big_screen.not().then(|| {
let send = self.send.clone();
let on_changed = Callback::from(move |s| {
let send = send.clone();
yew::platform::spawn_local(async move {
let mut send = send.clone();
if let Err(err) = send
.send(HostMessage::Lobby(HostLobbyMessage::SetGameSettings(s)))
.await
{
log::error!("sending game settings update: {err}");
}
if let Err(err) = send
.send(HostMessage::Lobby(HostLobbyMessage::GetGameSettings))
.await
{
log::error!("sending game settings get: {err}");
}
});
});
let send = self.send.clone();
let on_start = Callback::from(move |_| {
let send = send.clone();
let on_error = on_error.clone();
yew::platform::spawn_local(async move {
let mut send = send.clone();
if let Err(err) =
send.send(HostMessage::Lobby(HostLobbyMessage::Start)).await
{
on_error.emit(Some(err.into()))
}
});
});
html! {
<Settings
settings={settings}
on_start={on_start}
on_update={on_changed}
players_in_lobby={players.clone()}
/>
}
});
let on_action = self.big_screen.not().then(|| {
let on_error = self.error_callback.clone();
let send = self.send.clone();
Callback::from(move |(player_id, act): (PlayerId, LobbyPlayerAction)| {
let msg = match act {
LobbyPlayerAction::Kick => {
HostMessage::Lobby(HostLobbyMessage::Kick(player_id))
}
LobbyPlayerAction::SetNumber(num) => HostMessage::Lobby(
HostLobbyMessage::SetPlayerNumber(player_id, num),
),
};
let mut send = send.clone();
let on_error = on_error.clone();
yew::platform::spawn_local(async move {
if let Err(err) = send.send(msg).await {
on_error.emit(Some(err.into()))
}
});
})
});
html! {
<div class="column-list">
{settings}
<Lobby players={players} on_action={on_action}/>
</div>
if self.big_screen {
self.lobby_big_screen_show_setup(players, settings)
} else {
self.lobby_setup(players, settings)
}
}
HostState::Day {
@ -644,3 +578,83 @@ impl Component for Host {
}
}
}
impl Host {
fn lobby_big_screen_show_setup(&self, _: Rc<[PlayerState]>, settings: GameSettings) -> Html {
html! {
<Setup settings={settings}/>
}
}
fn lobby_setup(&self, players: Rc<[PlayerState]>, settings: GameSettings) -> Html {
let on_error = self.error_callback.clone();
let settings = self.big_screen.not().then(|| {
let send = self.send.clone();
let on_changed = Callback::from(move |s| {
let send = send.clone();
yew::platform::spawn_local(async move {
let mut send = send.clone();
if let Err(err) = send
.send(HostMessage::Lobby(HostLobbyMessage::SetGameSettings(s)))
.await
{
log::error!("sending game settings update: {err}");
}
if let Err(err) = send
.send(HostMessage::Lobby(HostLobbyMessage::GetGameSettings))
.await
{
log::error!("sending game settings get: {err}");
}
});
});
let send = self.send.clone();
let on_start = Callback::from(move |_| {
let send = send.clone();
let on_error = on_error.clone();
yew::platform::spawn_local(async move {
let mut send = send.clone();
if let Err(err) = send.send(HostMessage::Lobby(HostLobbyMessage::Start)).await {
on_error.emit(Some(err.into()))
}
});
});
html! {
<Settings
settings={settings}
on_start={on_start}
on_update={on_changed}
players_in_lobby={players.clone()}
/>
}
});
let on_action = self.big_screen.not().then(|| {
let on_error = self.error_callback.clone();
let send = self.send.clone();
Callback::from(move |(player_id, act): (PlayerId, LobbyPlayerAction)| {
let msg = match act {
LobbyPlayerAction::Kick => {
HostMessage::Lobby(HostLobbyMessage::Kick(player_id))
}
LobbyPlayerAction::SetNumber(num) => {
HostMessage::Lobby(HostLobbyMessage::SetPlayerNumber(player_id, num))
}
};
let mut send = send.clone();
let on_error = on_error.clone();
yew::platform::spawn_local(async move {
if let Err(err) = send.send(msg).await {
on_error.emit(Some(err.into()))
}
});
})
});
html! {
<div class="column-list">
{settings}
<Lobby players={players} on_action={on_action}/>
</div>
}
}
}

View File

@ -0,0 +1,162 @@
use core::ops::Not;
use std::collections::HashMap;
use rand::Rng;
use werewolves_proto::{
game::{Category, GameSettings, SetupRole, SetupRoleTitle},
role::Alignment,
};
use yew::prelude::*;
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct SetupProps {
pub settings: GameSettings,
}
#[function_component]
pub fn Setup(SetupProps { settings }: &SetupProps) -> Html {
let mut by_category: HashMap<Category, Vec<SetupRole>> =
Category::ALL.into_iter().map(|c| (c, Vec::new())).collect();
for slot in settings.slots() {
by_category
.get_mut(&slot.role.category())
.unwrap()
.push(slot.role.clone());
}
let mut categories = by_category.into_iter().collect::<Box<[_]>>();
categories.sort_by_key(|(c, _)| match c {
Category::Wolves => 1u8,
Category::Intel => 2,
Category::Villager => 4,
Category::Defensive => 3,
Category::Offensive => 5,
Category::StartsAsVillager => 6,
});
let categories = categories
.into_iter()
.map(|(cat, members)| {
let hide = match cat {
Category::Wolves => CategoryMode::ShowExactRoleCount,
Category::Villager => CategoryMode::ShowExactRoleCount,
Category::Intel
| Category::Defensive
| Category::Offensive
| Category::StartsAsVillager => CategoryMode::HideAllInfo,
};
html! {
<SetupCategory
category={cat}
roles={members.into_boxed_slice()}
mode={hide}
/>
}
})
.collect::<Html>();
let power_roles_count = settings
.slots()
.iter()
.filter(|r| !matches!(r.role.category(), Category::Villager | Category::Wolves))
.count();
html! {
<div class="setup-screen">
<div class="setup">
{categories}
</div>
<div class="category village">
<span class="count">{power_roles_count}</span>
<span class="title">{"Power roles from..."}</span>
</div>
</div>
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[allow(unused)]
pub enum CategoryMode {
#[default]
HideAllInfo,
ShowTotalCount,
ShowExactRoleCount,
}
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct SetupCategoryProps {
pub category: Category,
pub roles: Box<[SetupRole]>,
#[prop_or_default]
pub mode: CategoryMode,
}
#[function_component]
pub fn SetupCategory(
SetupCategoryProps {
category,
roles,
mode,
}: &SetupCategoryProps,
) -> Html {
let roles_count = match mode {
CategoryMode::HideAllInfo => None,
CategoryMode::ShowTotalCount | CategoryMode::ShowExactRoleCount => Some(html! {
<span class="count">{roles.len().to_string()}</span>
}),
};
let mut roles_in_category = SetupRoleTitle::ALL
.into_iter()
.filter(|r| r.category() == *category)
.collect::<Box<[_]>>();
roles_in_category.sort_by_key(|l| l.into_role().wakes_night_zero());
let all_roles = roles_in_category
.into_iter()
.map(|r| (r, roles.iter().filter(|sr| sr.title() == r).count()))
.filter(|(_, count)| !(matches!(mode, CategoryMode::ShowExactRoleCount) && *count == 0))
.map(|(r, count)| {
let as_role = r.into_role();
let wakes = as_role.wakes_night_zero().then_some("wakes");
let count = matches!(mode, CategoryMode::ShowExactRoleCount).then(|| {
html! {
<span class="count">{count}</span>
}
});
let killer_inactive = as_role.killer().not().then_some("inactive");
let powerful_inactive = as_role.powerful().not().then_some("inactive");
let alignment = match as_role.alignment() {
Alignment::Village => "/img/village.svg",
Alignment::Wolves => "/img/wolf.svg",
};
html! {
<div class={classes!("slot")}>
<div class={classes!("role", wakes, r.category().class())}>
{count}
{r.to_string()}
</div>
<div class="attributes">
<div class="alignment">
<img class="icon" src={alignment} alt={"alignment"}/>
</div>
<div class={classes!("killer", killer_inactive)}>
<img class="icon" src="/img/killer.svg" alt="killer icon"/>
</div>
<div class={classes!("poweful", powerful_inactive)}>
<img class="icon" src="/img/powerful.svg" alt="powerful icon"/>
</div>
</div>
</div>
}
})
.collect::<Html>();
html! {
<div class="category">
{roles_count}
<div class={classes!("title", category.class())}>{category.to_string()}</div>
<div class={classes!("category-list")}>
{all_roles}
</div>
</div>
}
}

View File

@ -212,9 +212,42 @@ pub fn Settings(
}
});
let clear_setup = {
let update = on_update.clone();
let on_click = Callback::from(move |_| {
update.emit(GameSettings::empty());
});
let disabled_reason = settings.slots().is_empty().then_some("no setup to clear");
html! {
<Button on_click={on_click} disabled_reason={disabled_reason}>
{"clear setup"}
</Button>
}
};
let fill_empty_with_villagers = {
let disabled_reason =
(settings.min_players_needed() >= players.len()).then_some("no empty slots");
let update = on_update.clone();
let settings = settings.clone();
let player_count = players.len();
let on_click = Callback::from(move |_| {
let mut settings = (*settings).clone();
settings.fill_remaining_slots_with_villagers(player_count);
update.emit(settings);
});
html! {
<Button on_click={on_click} disabled_reason={disabled_reason}>
{"fill empty slots with villagers"}
</Button>
}
};
html! {
<div class="settings">
<div class="top-settings">
{fill_empty_with_villagers}
{clear_setup}
{clear_all_assignments}
{clear_bad_assigned}
</div>
@ -376,7 +409,7 @@ fn setup_options_for_slot(
</>
})
}
SetupRole::Apprentice { specifically } => {
SetupRole::Apprentice { to: specifically } => {
let options = roles_in_setup
.iter()
.filter(|r| r.is_mentor())
@ -398,7 +431,7 @@ fn setup_options_for_slot(
let role_name = option.to_string().to_case(Case::Title);
let mut slot = slot.clone();
match &mut slot.role {
SetupRole::Apprentice { specifically } => {
SetupRole::Apprentice { to: specifically } => {
specifically.replace(option);
}
_ => unreachable!(),
@ -421,7 +454,7 @@ fn setup_options_for_slot(
let role_name = "random";
let mut slot = slot.clone();
match &mut slot.role {
SetupRole::Apprentice { specifically } => {
SetupRole::Apprentice { to: specifically } => {
specifically.take();
}
_ => unreachable!(),

View File

@ -55,7 +55,7 @@ fn main() {
}
} else if path.starts_with("/many-client") {
let clients = document.query_selector("clients").unwrap().unwrap();
for (player_id, name, num, dupe) in (1..=16).map(|num| {
for (player_id, name, num, dupe) in (1..=7).map(|num| {
(
PlayerId::from_u128(num as u128),
format!("player {num}"),