removed notorious, added more info to host setup
This commit is contained in:
parent
3d17a2d54b
commit
6dd78aa1b5
|
|
@ -53,8 +53,6 @@ pub enum Aura {
|
||||||
/// getting a first result of wolf/killer/powerful if seer/adjudicator/power seer
|
/// getting a first result of wolf/killer/powerful if seer/adjudicator/power seer
|
||||||
#[checks("assignable")]
|
#[checks("assignable")]
|
||||||
InevitableScapegoat,
|
InevitableScapegoat,
|
||||||
#[checks("assignable")]
|
|
||||||
Notorious,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Aura {
|
impl Display for Aura {
|
||||||
|
|
@ -69,7 +67,6 @@ impl Display for Aura {
|
||||||
Aura::VindictiveScapegoat => "Vindictive Scapegoat",
|
Aura::VindictiveScapegoat => "Vindictive Scapegoat",
|
||||||
Aura::SpitefulScapegoat => "Spiteful Scapegoat",
|
Aura::SpitefulScapegoat => "Spiteful Scapegoat",
|
||||||
Aura::InevitableScapegoat => "Inevitable Scapegoat",
|
Aura::InevitableScapegoat => "Inevitable Scapegoat",
|
||||||
Aura::Notorious => "Notorious",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +79,6 @@ impl Aura {
|
||||||
| Aura::VindictiveScapegoat
|
| Aura::VindictiveScapegoat
|
||||||
| Aura::SpitefulScapegoat
|
| Aura::SpitefulScapegoat
|
||||||
| Aura::InevitableScapegoat
|
| Aura::InevitableScapegoat
|
||||||
| Aura::Notorious
|
|
||||||
| Aura::Traitor
|
| Aura::Traitor
|
||||||
| Aura::Drunk(_)
|
| Aura::Drunk(_)
|
||||||
| Aura::Insane => false,
|
| Aura::Insane => false,
|
||||||
|
|
@ -171,8 +167,7 @@ impl Auras {
|
||||||
for aura in self.0.iter() {
|
for aura in self.0.iter() {
|
||||||
match aura {
|
match aura {
|
||||||
Aura::Traitor => return Some(Alignment::Traitor),
|
Aura::Traitor => return Some(Alignment::Traitor),
|
||||||
Aura::Notorious
|
Aura::RedeemableScapegoat
|
||||||
| Aura::RedeemableScapegoat
|
|
||||||
| Aura::VindictiveScapegoat
|
| Aura::VindictiveScapegoat
|
||||||
| Aura::SpitefulScapegoat
|
| Aura::SpitefulScapegoat
|
||||||
| Aura::Scapegoat
|
| Aura::Scapegoat
|
||||||
|
|
@ -212,7 +207,39 @@ impl AuraTitle {
|
||||||
AuraTitle::VindictiveScapegoat => Aura::VindictiveScapegoat,
|
AuraTitle::VindictiveScapegoat => Aura::VindictiveScapegoat,
|
||||||
AuraTitle::SpitefulScapegoat => Aura::SpitefulScapegoat,
|
AuraTitle::SpitefulScapegoat => Aura::SpitefulScapegoat,
|
||||||
AuraTitle::InevitableScapegoat => Aura::InevitableScapegoat,
|
AuraTitle::InevitableScapegoat => Aura::InevitableScapegoat,
|
||||||
AuraTitle::Notorious => Aura::Notorious,
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns a list of auras with the current aura added.
|
||||||
|
///
|
||||||
|
/// **this will remove incompatible auras**
|
||||||
|
pub fn try_add_to_list(&self, list: &[Self]) -> Vec<Self> {
|
||||||
|
list.iter()
|
||||||
|
.filter(|o| *o != self && !self.incompatible_with(**o))
|
||||||
|
.copied()
|
||||||
|
.chain(Some(*self))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// determines whether the `other` [AuraTitle] is incompatible with
|
||||||
|
/// this aura
|
||||||
|
pub fn incompatible_with(&self, other: Self) -> bool {
|
||||||
|
match self {
|
||||||
|
AuraTitle::Bloodlet | AuraTitle::Insane | AuraTitle::Drunk | AuraTitle::Traitor => {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
AuraTitle::VindictiveScapegoat
|
||||||
|
| AuraTitle::RedeemableScapegoat
|
||||||
|
| AuraTitle::SpitefulScapegoat
|
||||||
|
| AuraTitle::InevitableScapegoat
|
||||||
|
| AuraTitle::Scapegoat => matches!(
|
||||||
|
other,
|
||||||
|
AuraTitle::Scapegoat
|
||||||
|
| AuraTitle::RedeemableScapegoat
|
||||||
|
| AuraTitle::SpitefulScapegoat
|
||||||
|
| AuraTitle::VindictiveScapegoat
|
||||||
|
| AuraTitle::InevitableScapegoat
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -643,7 +643,6 @@ impl Night {
|
||||||
Aura::RedeemableScapegoat
|
Aura::RedeemableScapegoat
|
||||||
| Aura::VindictiveScapegoat
|
| Aura::VindictiveScapegoat
|
||||||
| Aura::SpitefulScapegoat
|
| Aura::SpitefulScapegoat
|
||||||
| Aura::Notorious
|
|
||||||
| Aura::Scapegoat
|
| Aura::Scapegoat
|
||||||
| Aura::Traitor
|
| Aura::Traitor
|
||||||
| Aura::Bloodlet { .. } => continue,
|
| Aura::Bloodlet { .. } => continue,
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,6 @@ impl SetupRoleTitle {
|
||||||
| AuraTitle::VindictiveScapegoat
|
| AuraTitle::VindictiveScapegoat
|
||||||
| AuraTitle::SpitefulScapegoat
|
| AuraTitle::SpitefulScapegoat
|
||||||
| AuraTitle::InevitableScapegoat
|
| AuraTitle::InevitableScapegoat
|
||||||
| AuraTitle::Notorious
|
|
||||||
| AuraTitle::Traitor
|
| AuraTitle::Traitor
|
||||||
| AuraTitle::Bloodlet
|
| AuraTitle::Bloodlet
|
||||||
| AuraTitle::Insane => false,
|
| AuraTitle::Insane => false,
|
||||||
|
|
@ -212,12 +211,10 @@ impl SetupRoleTitle {
|
||||||
| SetupRoleTitle::Insomniac
|
| SetupRoleTitle::Insomniac
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AuraTitle::Notorious => {
|
|
||||||
!matches!(self, SetupRoleTitle::Villager | SetupRoleTitle::Scapegoat)
|
|
||||||
}
|
|
||||||
AuraTitle::RedeemableScapegoat => matches!(self, SetupRoleTitle::Villager),
|
AuraTitle::RedeemableScapegoat => matches!(self, SetupRoleTitle::Villager),
|
||||||
AuraTitle::VindictiveScapegoat | AuraTitle::SpitefulScapegoat => true,
|
AuraTitle::VindictiveScapegoat
|
||||||
AuraTitle::Scapegoat => matches!(self, SetupRoleTitle::Villager),
|
| AuraTitle::SpitefulScapegoat
|
||||||
|
| AuraTitle::Scapegoat => !matches!(self, SetupRoleTitle::Scapegoat),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn into_role(self) -> Role {
|
pub fn into_role(self) -> Role {
|
||||||
|
|
|
||||||
|
|
@ -264,7 +264,7 @@ nav.host-nav {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&>label {
|
&>span {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -473,13 +473,17 @@ button.confirm {
|
||||||
}
|
}
|
||||||
|
|
||||||
.roles-in-setup {
|
.roles-in-setup {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
padding: 1rem;
|
||||||
padding: 10px;
|
border: 4px solid $village_color_faint;
|
||||||
|
background-color: color.change($village_color_faint, $alpha: 0.05);
|
||||||
|
|
||||||
|
zoom: 120%;
|
||||||
|
|
||||||
&>h3 {
|
&>h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: rgba(255, 255, 255, 0.6);
|
color: white;
|
||||||
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -862,11 +866,9 @@ clients {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.shown {
|
&.shown {
|
||||||
// visibility: visible;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
// position: absolute;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
|
@ -914,6 +916,31 @@ error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.identity-span {
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number {
|
||||||
|
padding-right: 5px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.binary {
|
.binary {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -1133,6 +1160,52 @@ input {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slot-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 3px;
|
||||||
|
font-size: 0.7em;
|
||||||
|
|
||||||
|
.slot-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 3px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
opacity: 30%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-auras {
|
||||||
|
padding-top: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
.slot-aura-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 3px;
|
||||||
|
|
||||||
|
span::after {
|
||||||
|
content: ', ';
|
||||||
|
}
|
||||||
|
|
||||||
|
span:last-child::after {
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.increment-decrement {
|
.increment-decrement {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
@ -1142,9 +1215,7 @@ input {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
|
|
||||||
&>label {
|
&>span {
|
||||||
// height: 100%;
|
|
||||||
// width: 100%;
|
|
||||||
flex-grow: 3;
|
flex-grow: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1158,8 +1229,7 @@ input {
|
||||||
.setup-slot {
|
.setup-slot {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
& button label {
|
& button span {
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1167,6 +1237,7 @@ input {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
|
||||||
&>.submenu {
|
&>.submenu {
|
||||||
width: 30vw;
|
width: 30vw;
|
||||||
|
|
@ -1199,6 +1270,19 @@ input {
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
|
||||||
|
.role-box {
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&>* {
|
||||||
|
flex-shrink: 1;
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1431,7 +1515,8 @@ input {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-around;
|
justify-content: flex-start;
|
||||||
|
gap: 5px;
|
||||||
row-gap: 10px;
|
row-gap: 10px;
|
||||||
|
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
|
|
@ -1463,71 +1548,106 @@ input {
|
||||||
border: 1px solid color.change($defensive_border, $lightness: 40%);
|
border: 1px solid color.change($defensive_border, $lightness: 40%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.category {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
width: 30%;
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
&.final {
|
}
|
||||||
margin-top: 1cm;
|
|
||||||
margin-bottom: 1cm;
|
|
||||||
|
|
||||||
|
.category {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&.add-list {
|
||||||
|
@media only screen and (max-width : 599px) {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .title {
|
@media only screen and (min-width : 600px) {
|
||||||
text-shadow: black 3px 2px;
|
width: 160px;
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& .count {
|
.hidden {
|
||||||
text-align: right;
|
display: none;
|
||||||
left: -40px;
|
}
|
||||||
position: relative;
|
|
||||||
|
|
||||||
width: 0;
|
width: auto;
|
||||||
height: 0;
|
flex-wrap: wrap;
|
||||||
|
gap: 1px;
|
||||||
|
|
||||||
.scapegoats {
|
|
||||||
color: rgba(255, 0, 255, 0.7);
|
&>.title {
|
||||||
font-size: 2em;
|
font-size: 0.5em !important;
|
||||||
position: absolute;
|
margin-bottom: 0px !important;
|
||||||
}
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-list {
|
.category-list {
|
||||||
text-align: left;
|
gap: 1px;
|
||||||
flex: 1, 1, 100%;
|
}
|
||||||
display: flex;
|
}
|
||||||
flex-wrap: nowrap;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
|
|
||||||
.slot {
|
&.big {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.final {
|
||||||
|
margin-top: 1cm;
|
||||||
|
margin-bottom: 1cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .title {
|
||||||
|
text-shadow: black 3px 2px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .count {
|
||||||
|
text-align: right;
|
||||||
|
left: -40px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
|
||||||
|
.scapegoats {
|
||||||
|
color: rgba(255, 0, 255, 0.7);
|
||||||
|
font-size: 2em;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-list {
|
||||||
|
text-align: left;
|
||||||
|
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;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.attributes {
|
.role {
|
||||||
margin-left: 10px;
|
text-shadow: black 3px 2px;
|
||||||
align-self: flex-end;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role {
|
width: 100%;
|
||||||
text-shadow: black 3px 2px;
|
filter: saturate(40%);
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
|
||||||
width: 100%;
|
&.wakes {
|
||||||
filter: saturate(40%);
|
border: 2px solid yellow;
|
||||||
padding-left: 10px;
|
|
||||||
padding-right: 10px;
|
|
||||||
|
|
||||||
&.wakes {
|
|
||||||
border: 2px solid yellow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2521,3 +2641,10 @@ li.choice {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.option-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,10 @@ use yew::{html::Scope, prelude::*};
|
||||||
use crate::{
|
use crate::{
|
||||||
callback,
|
callback,
|
||||||
components::{
|
components::{
|
||||||
Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Settings, Story, Victory,
|
Button, Footer, Lobby, LobbyPlayerAction, RoleReveal, Story, Victory,
|
||||||
action::{ActionResultView, Prompt},
|
action::{ActionResultView, Prompt},
|
||||||
host::{CharacterStatesReadOnly, DaytimePlayerList, Setup},
|
host::{CharacterStatesReadOnly, DaytimePlayerList, Setup},
|
||||||
|
settings::Settings,
|
||||||
},
|
},
|
||||||
pages::RolePage,
|
pages::RolePage,
|
||||||
storage::StorageKey,
|
storage::StorageKey,
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ impl Class for AuraTitle {
|
||||||
AuraTitle::RedeemableScapegoat
|
AuraTitle::RedeemableScapegoat
|
||||||
| AuraTitle::SpitefulScapegoat
|
| AuraTitle::SpitefulScapegoat
|
||||||
| AuraTitle::VindictiveScapegoat
|
| AuraTitle::VindictiveScapegoat
|
||||||
| AuraTitle::Notorious
|
|
||||||
| AuraTitle::InevitableScapegoat
|
| AuraTitle::InevitableScapegoat
|
||||||
| AuraTitle::Scapegoat => "scapegoat",
|
| AuraTitle::Scapegoat => "scapegoat",
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ pub fn Setup(SetupProps { settings }: &SetupProps) -> Html {
|
||||||
<div class="setup">
|
<div class="setup">
|
||||||
{categories}
|
{categories}
|
||||||
</div>
|
</div>
|
||||||
<div class="category village final">
|
<div class="category big village final">
|
||||||
<span class="count">{power_roles_count}</span>
|
<span class="count">{power_roles_count}</span>
|
||||||
<span class="title">{"Power roles from..."}</span>
|
<span class="title">{"Power roles from..."}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -208,7 +208,7 @@ pub fn SetupCategory(
|
||||||
})
|
})
|
||||||
.collect::<Html>();
|
.collect::<Html>();
|
||||||
html! {
|
html! {
|
||||||
<div class="category">
|
<div class="category big">
|
||||||
{roles_count}
|
{roles_count}
|
||||||
<div class={classes!("title", category.class())}>
|
<div class={classes!("title", category.class())}>
|
||||||
{category.to_string().to_case(Case::Title)}
|
{category.to_string().to_case(Case::Title)}
|
||||||
|
|
|
||||||
|
|
@ -241,8 +241,7 @@ impl PartialAssociatedIcon for AuraTitle {
|
||||||
| AuraTitle::RedeemableScapegoat
|
| AuraTitle::RedeemableScapegoat
|
||||||
| AuraTitle::VindictiveScapegoat
|
| AuraTitle::VindictiveScapegoat
|
||||||
| AuraTitle::SpitefulScapegoat
|
| AuraTitle::SpitefulScapegoat
|
||||||
| AuraTitle::InevitableScapegoat
|
| AuraTitle::InevitableScapegoat => Some(IconSource::Scapegoat),
|
||||||
| AuraTitle::Notorious => Some(IconSource::Scapegoat),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,3 +50,33 @@ pub fn Identity(props: &IdentityProps) -> Html {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn IdentitySpan(
|
||||||
|
IdentityProps {
|
||||||
|
ident:
|
||||||
|
PublicIdentity {
|
||||||
|
name,
|
||||||
|
pronouns,
|
||||||
|
number,
|
||||||
|
},
|
||||||
|
class,
|
||||||
|
}: &IdentityProps,
|
||||||
|
) -> Html {
|
||||||
|
let pronouns = pronouns.as_ref().map(|p| {
|
||||||
|
html! {
|
||||||
|
<span class="pronouns">{"("}{p}{")"}</span>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let not_set = number.is_none().then_some("not-set");
|
||||||
|
let number = number
|
||||||
|
.map(|n| n.to_string())
|
||||||
|
.unwrap_or_else(|| String::from("???"));
|
||||||
|
html! {
|
||||||
|
<div class={classes!("identity-span", class.clone())}>
|
||||||
|
<span class={classes!("number", not_set)}><b>{number}</b></span>
|
||||||
|
<span>{name}</span>
|
||||||
|
{pronouns}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use werewolves_proto::role::RoleTitle;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::Button;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct AddRoleCategoryProps {
|
||||||
|
pub category: werewolves_proto::game::Category,
|
||||||
|
pub roles: Box<[RoleTitle]>,
|
||||||
|
pub add_role: Callback<RoleTitle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn AddRoleCategory(
|
||||||
|
AddRoleCategoryProps {
|
||||||
|
category,
|
||||||
|
roles,
|
||||||
|
add_role,
|
||||||
|
}: &AddRoleCategoryProps,
|
||||||
|
) -> Html {
|
||||||
|
let class = category.class();
|
||||||
|
let hidden = use_state(|| true);
|
||||||
|
|
||||||
|
let roles = roles
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|title| {
|
||||||
|
let on_click = {
|
||||||
|
let add_role = add_role.clone();
|
||||||
|
let role = title;
|
||||||
|
Callback::from(move |_| add_role.emit(role))
|
||||||
|
};
|
||||||
|
let name = title.to_string().to_case(Case::Title);
|
||||||
|
html! {
|
||||||
|
<div class="slot">
|
||||||
|
<Button on_click={on_click} classes={classes!("role", class)}>
|
||||||
|
{name}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
|
||||||
|
let on_toggle = {
|
||||||
|
let hidden = hidden.clone();
|
||||||
|
Callback::from(move |_| hidden.set(!*hidden))
|
||||||
|
};
|
||||||
|
let hidden = (*hidden).then_some("hidden");
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="category add-list">
|
||||||
|
<div class={classes!("title", class)} onclick={on_toggle}>
|
||||||
|
{category.to_string().to_case(Case::Sentence)}
|
||||||
|
</div>
|
||||||
|
<div class={classes!("category-list", hidden)}>
|
||||||
|
{roles}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,350 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use core::{num::NonZeroU8, ops::Not};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use werewolves_proto::{
|
||||||
|
aura::AuraTitle,
|
||||||
|
error::GameError,
|
||||||
|
game::{Category, GameSettings, OrRandom, SetupRole, SetupSlot, SlotId},
|
||||||
|
message::{Identification, PlayerState, PublicIdentity},
|
||||||
|
role::RoleTitle,
|
||||||
|
};
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
class::Class,
|
||||||
|
components::{
|
||||||
|
Button, ClickableField, Icon, IconType, Identity, PartialAssociatedIcon,
|
||||||
|
client::Signin,
|
||||||
|
settings::{AddRoleCategory, SettingSlotAction, SettingsSlot},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Properties)]
|
||||||
|
pub struct SettingsProps {
|
||||||
|
pub settings: GameSettings,
|
||||||
|
pub players_in_lobby: Rc<[PlayerState]>,
|
||||||
|
pub on_update: Callback<GameSettings>,
|
||||||
|
pub on_start: Callback<()>,
|
||||||
|
pub on_add_player: Callback<PublicIdentity>,
|
||||||
|
pub qr_mode_button: Html,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn Settings(
|
||||||
|
SettingsProps {
|
||||||
|
settings,
|
||||||
|
players_in_lobby,
|
||||||
|
on_update,
|
||||||
|
on_start,
|
||||||
|
on_add_player,
|
||||||
|
qr_mode_button,
|
||||||
|
}: &SettingsProps,
|
||||||
|
) -> Html {
|
||||||
|
let players = players_in_lobby
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.identification.clone())
|
||||||
|
.collect::<Rc<[_]>>();
|
||||||
|
let disabled_reason = settings
|
||||||
|
.check_with_player_list(&players)
|
||||||
|
.err()
|
||||||
|
.map(|err| err.to_string());
|
||||||
|
|
||||||
|
let roles_in_setup: Rc<[RoleTitle]> = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.map(|s| Into::<RoleTitle>::into(s.role.clone()))
|
||||||
|
.collect();
|
||||||
|
let on_update_role = on_update.clone();
|
||||||
|
let settings = Rc::new(settings.clone());
|
||||||
|
let on_update_role_settings = settings.clone();
|
||||||
|
let already_assigned_pids = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|r| r.assign_to)
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
let players_for_assign = players
|
||||||
|
.iter()
|
||||||
|
.filter(|p| !already_assigned_pids.contains(&p.player_id))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Rc<[_]>>();
|
||||||
|
let update_role_card = Callback::from(move |act: SettingSlotAction| {
|
||||||
|
let mut new_settings = (*on_update_role_settings).clone();
|
||||||
|
match act {
|
||||||
|
SettingSlotAction::Remove(slot_id) => new_settings.remove_slot(slot_id),
|
||||||
|
SettingSlotAction::Update(slot) => new_settings.update_slot(slot),
|
||||||
|
}
|
||||||
|
on_update_role.emit(new_settings);
|
||||||
|
});
|
||||||
|
let roles = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.map(|slot| {
|
||||||
|
html! {
|
||||||
|
<SettingsSlot
|
||||||
|
all_players={players.clone()}
|
||||||
|
players_for_assign={players_for_assign.clone()}
|
||||||
|
roles_in_setup={roles_in_setup.clone()}
|
||||||
|
slot={slot.clone()}
|
||||||
|
update={update_role_card.clone()}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
|
||||||
|
let add_roles_update = on_update.clone();
|
||||||
|
|
||||||
|
let add_role = {
|
||||||
|
let settings = settings.clone();
|
||||||
|
let update = add_roles_update.clone();
|
||||||
|
Callback::from(move |role: RoleTitle| {
|
||||||
|
let mut settings = (*settings).clone();
|
||||||
|
settings.new_slot(role);
|
||||||
|
update.emit(settings);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let add_roles_buttons = Category::ALL
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|cat| {
|
||||||
|
let roles = cat
|
||||||
|
.entire_category()
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| c.into_role().title())
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
html! {
|
||||||
|
<AddRoleCategory category={cat} roles={roles} add_role={add_role.clone()}/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
|
||||||
|
let clear_bad_assigned = matches!(
|
||||||
|
settings.check_with_player_list(&players),
|
||||||
|
Err(GameError::AssignedMultipleTimes(_, _)) | Err(GameError::AssignedPlayerMissing(_))
|
||||||
|
)
|
||||||
|
.then(|| {
|
||||||
|
let clear_settings = settings.clone();
|
||||||
|
let clear_update = on_update.clone();
|
||||||
|
let clear_players = players.clone();
|
||||||
|
let clear_bad_assigned = Callback::from(move |_| {
|
||||||
|
let mut settings = (*clear_settings).clone();
|
||||||
|
settings.remove_assignments_not_in_list(&clear_players);
|
||||||
|
settings.remove_duplicate_assignments();
|
||||||
|
clear_update.emit(settings);
|
||||||
|
});
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<Button on_click={clear_bad_assigned}>
|
||||||
|
{"clear bad assignments"}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let clear_all_assignments = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.assign_to.is_some())
|
||||||
|
.then(|| {
|
||||||
|
let settings = settings.clone();
|
||||||
|
let update = on_update.clone();
|
||||||
|
let on_click = Callback::from(move |_| {
|
||||||
|
let mut settings = (*settings).clone();
|
||||||
|
settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.filter(|s| s.assign_to.is_some())
|
||||||
|
.map(|s| {
|
||||||
|
let mut s = s.clone();
|
||||||
|
s.assign_to.take();
|
||||||
|
s
|
||||||
|
})
|
||||||
|
.collect::<Box<[_]>>()
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|s| settings.update_slot(s));
|
||||||
|
update.emit(settings);
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<Button on_click={on_click}>{"clear all assignments"}</Button>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let assignments = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.assign_to.is_some())
|
||||||
|
.then(|| {
|
||||||
|
let assignments =
|
||||||
|
settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.filter_map(|s| {
|
||||||
|
s.assign_to
|
||||||
|
.as_ref()
|
||||||
|
.map(|a| {
|
||||||
|
players
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.player_id == *a)
|
||||||
|
.map(|assign| {
|
||||||
|
let class = s.role.category().class();
|
||||||
|
(html! {
|
||||||
|
<Identity ident={assign.public.clone()}/>
|
||||||
|
}, Some(class))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
(html! {
|
||||||
|
<span>{"[left the lobby]"}</span>
|
||||||
|
}, None)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(|(who, class)| {
|
||||||
|
let assignments_update = on_update.clone();
|
||||||
|
let assignments_settings = settings.clone();
|
||||||
|
let click_slot = s.clone();
|
||||||
|
let on_click = Callback::from(move |_| {
|
||||||
|
let mut settings = (*assignments_settings).clone();
|
||||||
|
let mut click_slot = click_slot.clone();
|
||||||
|
click_slot.assign_to.take();
|
||||||
|
settings.update_slot(click_slot);
|
||||||
|
assignments_update.emit(settings);
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<Button classes={classes!("assignment", class)} on_click={on_click}>
|
||||||
|
<label>{s.role.to_string().to_case(Case::Title)}</label>
|
||||||
|
{who}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<label>{"assignments"}</label>
|
||||||
|
<div class="assignments">
|
||||||
|
{assignments}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let add_player_open = use_state(|| false);
|
||||||
|
let add_player_opts = html! {
|
||||||
|
<div class="add-player">
|
||||||
|
<Signin callback={on_add_player.clone()}/>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
let current_roles = settings.slots().len();
|
||||||
|
let min_roles = settings.min_players_needed();
|
||||||
|
let min_roles_class = (current_roles < min_roles).then_some("red");
|
||||||
|
let player_count = players_in_lobby.len();
|
||||||
|
let player_count_class = (player_count != current_roles).then_some("red");
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="settings">
|
||||||
|
<div class="top-settings">
|
||||||
|
{qr_mode_button.clone()}
|
||||||
|
{fill_empty_with_villagers}
|
||||||
|
{clear_setup}
|
||||||
|
{clear_all_assignments}
|
||||||
|
{clear_bad_assigned}
|
||||||
|
</div>
|
||||||
|
<ClickableField
|
||||||
|
options={add_player_opts}
|
||||||
|
state={add_player_open}
|
||||||
|
>
|
||||||
|
{"add player"}
|
||||||
|
</ClickableField>
|
||||||
|
|
||||||
|
<div class="roles-add-list">
|
||||||
|
{add_roles_buttons}
|
||||||
|
</div>
|
||||||
|
<div class="settings-info">
|
||||||
|
<span class="current-min-role-count">
|
||||||
|
<span class="dimmed">{"current/minimum roles: "}</span>
|
||||||
|
<span class={classes!(min_roles_class)}>
|
||||||
|
<span class="current-roles">
|
||||||
|
{current_roles}
|
||||||
|
</span>
|
||||||
|
{"/"}
|
||||||
|
<span class="min-roles">
|
||||||
|
{min_roles}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="player-role-count">
|
||||||
|
<span class="dimmed">{"players/roles: "}</span>
|
||||||
|
<span class={classes!(player_count_class)}>
|
||||||
|
<span>
|
||||||
|
{player_count}
|
||||||
|
</span>
|
||||||
|
{"/"}
|
||||||
|
<span>
|
||||||
|
{current_roles}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="roles-in-setup">
|
||||||
|
<h3>{"roles in the game"}</h3>
|
||||||
|
<div class="role-list">
|
||||||
|
{roles}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{assignments}
|
||||||
|
<Button
|
||||||
|
disabled_reason={disabled_reason}
|
||||||
|
classes={classes!("start-game")}
|
||||||
|
on_click={on_start.clone()}
|
||||||
|
>
|
||||||
|
{"start game"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,348 +12,27 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use core::{num::NonZeroU8, ops::Not};
|
use core::{num::NonZeroU8, ops::Not};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
aura::AuraTitle,
|
aura::AuraTitle,
|
||||||
error::GameError,
|
game::{OrRandom, SetupRole, SetupSlot, SlotId},
|
||||||
game::{GameSettings, OrRandom, SetupRole, SetupSlot, SlotId},
|
message::Identification,
|
||||||
message::{Identification, PlayerState, PublicIdentity},
|
|
||||||
role::RoleTitle,
|
role::RoleTitle,
|
||||||
};
|
};
|
||||||
|
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
class::Class,
|
class::Class,
|
||||||
components::{
|
components::{
|
||||||
Button, ClickableField, Icon, IconType, Identity, PartialAssociatedIcon, client::Signin,
|
Button, ClickableField, Icon, IconType, Identity, IdentitySpan, PartialAssociatedIcon,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Properties)]
|
|
||||||
pub struct SettingsProps {
|
|
||||||
pub settings: GameSettings,
|
|
||||||
pub players_in_lobby: Rc<[PlayerState]>,
|
|
||||||
pub on_update: Callback<GameSettings>,
|
|
||||||
pub on_start: Callback<()>,
|
|
||||||
pub on_add_player: Callback<PublicIdentity>,
|
|
||||||
pub qr_mode_button: Html,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component]
|
|
||||||
pub fn Settings(
|
|
||||||
SettingsProps {
|
|
||||||
settings,
|
|
||||||
players_in_lobby,
|
|
||||||
on_update,
|
|
||||||
on_start,
|
|
||||||
on_add_player,
|
|
||||||
qr_mode_button,
|
|
||||||
}: &SettingsProps,
|
|
||||||
) -> Html {
|
|
||||||
let players = players_in_lobby
|
|
||||||
.iter()
|
|
||||||
.map(|p| p.identification.clone())
|
|
||||||
.collect::<Rc<[_]>>();
|
|
||||||
let disabled_reason = settings
|
|
||||||
.check_with_player_list(&players)
|
|
||||||
.err()
|
|
||||||
.map(|err| err.to_string());
|
|
||||||
|
|
||||||
let roles_in_setup: Rc<[RoleTitle]> = settings
|
|
||||||
.slots()
|
|
||||||
.iter()
|
|
||||||
.map(|s| Into::<RoleTitle>::into(s.role.clone()))
|
|
||||||
.collect();
|
|
||||||
let on_update_role = on_update.clone();
|
|
||||||
let settings = Rc::new(settings.clone());
|
|
||||||
let on_update_role_settings = settings.clone();
|
|
||||||
let already_assigned_pids = settings
|
|
||||||
.slots()
|
|
||||||
.iter()
|
|
||||||
.filter_map(|r| r.assign_to)
|
|
||||||
.collect::<Box<[_]>>();
|
|
||||||
let players_for_assign = players
|
|
||||||
.iter()
|
|
||||||
.filter(|p| !already_assigned_pids.contains(&p.player_id))
|
|
||||||
.cloned()
|
|
||||||
.collect::<Rc<[_]>>();
|
|
||||||
let update_role_card = Callback::from(move |act: SettingSlotAction| {
|
|
||||||
let mut new_settings = (*on_update_role_settings).clone();
|
|
||||||
match act {
|
|
||||||
SettingSlotAction::Remove(slot_id) => new_settings.remove_slot(slot_id),
|
|
||||||
SettingSlotAction::Update(slot) => new_settings.update_slot(slot),
|
|
||||||
}
|
|
||||||
on_update_role.emit(new_settings);
|
|
||||||
});
|
|
||||||
let roles = settings
|
|
||||||
.slots()
|
|
||||||
.iter()
|
|
||||||
.map(|slot| {
|
|
||||||
html! {
|
|
||||||
<SettingsSlot
|
|
||||||
all_players={players.clone()}
|
|
||||||
players_for_assign={players_for_assign.clone()}
|
|
||||||
roles_in_setup={roles_in_setup.clone()}
|
|
||||||
slot={slot.clone()}
|
|
||||||
update={update_role_card.clone()}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Html>();
|
|
||||||
|
|
||||||
let add_roles_update = on_update.clone();
|
|
||||||
let sorted_role_tiles = {
|
|
||||||
let mut v = RoleTitle::ALL.to_vec();
|
|
||||||
v.sort_by_key(|v| Into::<SetupRole>::into(*v).category());
|
|
||||||
v
|
|
||||||
};
|
|
||||||
let add_roles_buttons = sorted_role_tiles
|
|
||||||
.into_iter()
|
|
||||||
.map(|r| {
|
|
||||||
let update = add_roles_update.clone();
|
|
||||||
let settings = settings.clone();
|
|
||||||
let on_click = Callback::from(move |_| {
|
|
||||||
let mut settings = (*settings).clone();
|
|
||||||
settings.new_slot(r);
|
|
||||||
update.emit(settings);
|
|
||||||
});
|
|
||||||
let class = Into::<SetupRole>::into(r).category().class();
|
|
||||||
let name = r.to_string().to_case(Case::Title);
|
|
||||||
// let icon = r.icon().map(|icon| {
|
|
||||||
// html! {
|
|
||||||
// <Icon source={icon} icon_type={IconType::Small}/>
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
html! {
|
|
||||||
<button
|
|
||||||
onclick={on_click}
|
|
||||||
class={classes!(class, "add-role")}
|
|
||||||
>
|
|
||||||
<span>{name}</span>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Html>();
|
|
||||||
|
|
||||||
let clear_bad_assigned = matches!(
|
|
||||||
settings.check_with_player_list(&players),
|
|
||||||
Err(GameError::AssignedMultipleTimes(_, _)) | Err(GameError::AssignedPlayerMissing(_))
|
|
||||||
)
|
|
||||||
.then(|| {
|
|
||||||
let clear_settings = settings.clone();
|
|
||||||
let clear_update = on_update.clone();
|
|
||||||
let clear_players = players.clone();
|
|
||||||
let clear_bad_assigned = Callback::from(move |_| {
|
|
||||||
let mut settings = (*clear_settings).clone();
|
|
||||||
settings.remove_assignments_not_in_list(&clear_players);
|
|
||||||
settings.remove_duplicate_assignments();
|
|
||||||
clear_update.emit(settings);
|
|
||||||
});
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<Button on_click={clear_bad_assigned}>
|
|
||||||
{"clear bad assignments"}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let clear_all_assignments = settings
|
|
||||||
.slots()
|
|
||||||
.iter()
|
|
||||||
.any(|s| s.assign_to.is_some())
|
|
||||||
.then(|| {
|
|
||||||
let settings = settings.clone();
|
|
||||||
let update = on_update.clone();
|
|
||||||
let on_click = Callback::from(move |_| {
|
|
||||||
let mut settings = (*settings).clone();
|
|
||||||
settings
|
|
||||||
.slots()
|
|
||||||
.iter()
|
|
||||||
.filter(|s| s.assign_to.is_some())
|
|
||||||
.map(|s| {
|
|
||||||
let mut s = s.clone();
|
|
||||||
s.assign_to.take();
|
|
||||||
s
|
|
||||||
})
|
|
||||||
.collect::<Box<[_]>>()
|
|
||||||
.into_iter()
|
|
||||||
.for_each(|s| settings.update_slot(s));
|
|
||||||
update.emit(settings);
|
|
||||||
});
|
|
||||||
html! {
|
|
||||||
<Button on_click={on_click}>{"clear all assignments"}</Button>
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let assignments = settings
|
|
||||||
.slots()
|
|
||||||
.iter()
|
|
||||||
.any(|s| s.assign_to.is_some())
|
|
||||||
.then(|| {
|
|
||||||
let assignments =
|
|
||||||
settings
|
|
||||||
.slots()
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.filter_map(|s| {
|
|
||||||
s.assign_to
|
|
||||||
.as_ref()
|
|
||||||
.map(|a| {
|
|
||||||
players
|
|
||||||
.iter()
|
|
||||||
.find(|p| p.player_id == *a)
|
|
||||||
.map(|assign| {
|
|
||||||
let class = s.role.category().class();
|
|
||||||
(html! {
|
|
||||||
<Identity ident={assign.public.clone()}/>
|
|
||||||
}, Some(class))
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
(html! {
|
|
||||||
<span>{"[left the lobby]"}</span>
|
|
||||||
}, None)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.map(|(who, class)| {
|
|
||||||
let assignments_update = on_update.clone();
|
|
||||||
let assignments_settings = settings.clone();
|
|
||||||
let click_slot = s.clone();
|
|
||||||
let on_click = Callback::from(move |_| {
|
|
||||||
let mut settings = (*assignments_settings).clone();
|
|
||||||
let mut click_slot = click_slot.clone();
|
|
||||||
click_slot.assign_to.take();
|
|
||||||
settings.update_slot(click_slot);
|
|
||||||
assignments_update.emit(settings);
|
|
||||||
});
|
|
||||||
html! {
|
|
||||||
<Button classes={classes!("assignment", class)} on_click={on_click}>
|
|
||||||
<label>{s.role.to_string().to_case(Case::Title)}</label>
|
|
||||||
{who}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Html>();
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<>
|
|
||||||
<label>{"assignments"}</label>
|
|
||||||
<div class="assignments">
|
|
||||||
{assignments}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let add_player_open = use_state(|| false);
|
|
||||||
let add_player_opts = html! {
|
|
||||||
<div class="add-player">
|
|
||||||
<Signin callback={on_add_player.clone()}/>
|
|
||||||
</div>
|
|
||||||
};
|
|
||||||
let current_roles = settings.slots().len();
|
|
||||||
let min_roles = settings.min_players_needed();
|
|
||||||
let min_roles_class = (current_roles < min_roles).then_some("red");
|
|
||||||
let player_count = players_in_lobby.len();
|
|
||||||
let player_count_class = (player_count != current_roles).then_some("red");
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<div class="settings">
|
|
||||||
<div class="top-settings">
|
|
||||||
{qr_mode_button.clone()}
|
|
||||||
{fill_empty_with_villagers}
|
|
||||||
{clear_setup}
|
|
||||||
{clear_all_assignments}
|
|
||||||
{clear_bad_assigned}
|
|
||||||
</div>
|
|
||||||
<ClickableField
|
|
||||||
options={add_player_opts}
|
|
||||||
state={add_player_open}
|
|
||||||
>
|
|
||||||
{"add player"}
|
|
||||||
</ClickableField>
|
|
||||||
|
|
||||||
<div class="roles-add-list">
|
|
||||||
{add_roles_buttons}
|
|
||||||
</div>
|
|
||||||
<div class="settings-info">
|
|
||||||
<span class="current-min-role-count">
|
|
||||||
<span class="dimmed">{"current/minimum roles: "}</span>
|
|
||||||
<span class={classes!(min_roles_class)}>
|
|
||||||
<span class="current-roles">
|
|
||||||
{current_roles}
|
|
||||||
</span>
|
|
||||||
{"/"}
|
|
||||||
<span class="min-roles">
|
|
||||||
{min_roles}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span class="player-role-count">
|
|
||||||
<span class="dimmed">{"players/roles: "}</span>
|
|
||||||
<span class={classes!(player_count_class)}>
|
|
||||||
<span>
|
|
||||||
{player_count}
|
|
||||||
</span>
|
|
||||||
{"/"}
|
|
||||||
<span>
|
|
||||||
{current_roles}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="roles-in-setup">
|
|
||||||
<h3>{"roles in the game"}</h3>
|
|
||||||
<div class="role-list">
|
|
||||||
{roles}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{assignments}
|
|
||||||
<Button
|
|
||||||
disabled_reason={disabled_reason}
|
|
||||||
classes={classes!("start-game")}
|
|
||||||
on_click={on_start.clone()}
|
|
||||||
>
|
|
||||||
{"start game"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum SettingSlotAction {
|
pub enum SettingSlotAction {
|
||||||
Remove(SlotId),
|
Remove(SlotId),
|
||||||
Update(SetupSlot),
|
Update(SetupSlot),
|
||||||
|
|
@ -418,7 +97,7 @@ pub fn SettingsSlot(
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| html! {{"assign"}});
|
.unwrap_or_else(|| html! {{"assign"}});
|
||||||
html! {
|
html! {
|
||||||
<>
|
<div class="option-menu">
|
||||||
<Button on_click={on_kick} classes={classes!("red")}>
|
<Button on_click={on_kick} classes={classes!("red")}>
|
||||||
{"remove"}
|
{"remove"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -430,20 +109,62 @@ pub fn SettingsSlot(
|
||||||
{assign_text}
|
{assign_text}
|
||||||
</ClickableField>
|
</ClickableField>
|
||||||
{options}
|
{options}
|
||||||
</>
|
</div>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let class = slot.role.category().class();
|
let class = slot.role.category().class();
|
||||||
|
|
||||||
|
let assigned_to = slot
|
||||||
|
.assign_to
|
||||||
|
.as_ref()
|
||||||
|
.copied()
|
||||||
|
.and_then(|assigned_to| all_players.iter().find(|p| p.player_id == assigned_to))
|
||||||
|
.map(|ident| {
|
||||||
|
html! {
|
||||||
|
<IdentitySpan ident={ident.public.clone()}/>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let auras = slot.auras.is_empty().not().then_some({
|
||||||
|
let list = slot
|
||||||
|
.auras
|
||||||
|
.iter()
|
||||||
|
.map(|aura| {
|
||||||
|
html! {
|
||||||
|
<span>{aura.to_string()}</span>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
let title = (slot.auras.len() > 1).then_some(html! {
|
||||||
|
<span>{"auras:"}</span>
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<div class="slot-auras">
|
||||||
|
{title}
|
||||||
|
<div class="slot-aura-list">
|
||||||
|
{list}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let other_options = display_options_for_slot(slot);
|
||||||
html! {
|
html! {
|
||||||
<ClickableField
|
<div class="slot-container">
|
||||||
class={classes!("setup-slot")}
|
<ClickableField
|
||||||
button_class={classes!(class)}
|
class={classes!("setup-slot")}
|
||||||
options={submenu}
|
button_class={classes!(class, "faint")}
|
||||||
state={open}
|
options={submenu}
|
||||||
with_backdrop_exit=true
|
state={open}
|
||||||
>
|
with_backdrop_exit=true
|
||||||
<label>{role_name}</label>
|
>
|
||||||
</ClickableField>
|
<span>{role_name}</span>
|
||||||
|
</ClickableField>
|
||||||
|
<div class="slot-options">
|
||||||
|
{assigned_to}
|
||||||
|
{auras}
|
||||||
|
{other_options}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -481,6 +202,81 @@ fn assign_to_submenu(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn display_options_for_slot(slot: &SetupSlot) -> Html {
|
||||||
|
let options = match &slot.role {
|
||||||
|
SetupRole::Seer
|
||||||
|
| SetupRole::Arcanist
|
||||||
|
| SetupRole::Gravedigger
|
||||||
|
| SetupRole::Hunter
|
||||||
|
| SetupRole::Militia
|
||||||
|
| SetupRole::MapleWolf
|
||||||
|
| SetupRole::Guardian
|
||||||
|
| SetupRole::Protector
|
||||||
|
| SetupRole::Werewolf
|
||||||
|
| SetupRole::AlphaWolf
|
||||||
|
| SetupRole::DireWolf
|
||||||
|
| SetupRole::Shapeshifter
|
||||||
|
| SetupRole::LoneWolf
|
||||||
|
| SetupRole::Bloodletter
|
||||||
|
| SetupRole::Adjudicator
|
||||||
|
| SetupRole::Insomniac
|
||||||
|
| SetupRole::PowerSeer
|
||||||
|
| SetupRole::Mortician
|
||||||
|
| SetupRole::Beholder
|
||||||
|
| SetupRole::Empath
|
||||||
|
| SetupRole::Vindicator
|
||||||
|
| SetupRole::Diseased
|
||||||
|
| SetupRole::BlackKnight
|
||||||
|
| SetupRole::Weightlifter
|
||||||
|
| SetupRole::PyreMaster
|
||||||
|
| SetupRole::Villager => return html! {},
|
||||||
|
SetupRole::Elder { knows_on_night } => html! {
|
||||||
|
<span>{"wakes night "}{knows_on_night.get()}</span>
|
||||||
|
},
|
||||||
|
SetupRole::MasonLeader { recruits_available } => html! {
|
||||||
|
<span>{recruits_available.get()}{
|
||||||
|
if recruits_available.get() == 1 {
|
||||||
|
" recruit"
|
||||||
|
} else {
|
||||||
|
" recruits"
|
||||||
|
}
|
||||||
|
}</span>
|
||||||
|
},
|
||||||
|
SetupRole::Apprentice { to: None } => html! {
|
||||||
|
<span>{"random mentor"}</span>
|
||||||
|
},
|
||||||
|
SetupRole::Apprentice { to: Some(mentor) } => {
|
||||||
|
let class = Into::<SetupRole>::into(*mentor).category().class();
|
||||||
|
html! {
|
||||||
|
<span>
|
||||||
|
{"apprentice to "}
|
||||||
|
<span class={classes!(class, "faint")}>{mentor.to_string().to_case(Case::Sentence)}</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SetupRole::Scapegoat {
|
||||||
|
redeemed: OrRandom::Random,
|
||||||
|
} => html! {
|
||||||
|
<span>{"redemption random"}</span>
|
||||||
|
},
|
||||||
|
SetupRole::Scapegoat {
|
||||||
|
redeemed: OrRandom::Determined(false),
|
||||||
|
} => html! {
|
||||||
|
<span>{"not redeemed"}</span>
|
||||||
|
},
|
||||||
|
SetupRole::Scapegoat {
|
||||||
|
redeemed: OrRandom::Determined(true),
|
||||||
|
} => html! {
|
||||||
|
<span>{"redeemed"}</span>
|
||||||
|
},
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<div class="slot-options-display">
|
||||||
|
{options}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn setup_options_for_slot(
|
fn setup_options_for_slot(
|
||||||
slot: &SetupSlot,
|
slot: &SetupSlot,
|
||||||
update: &Callback<SettingSlotAction>,
|
update: &Callback<SettingSlotAction>,
|
||||||
|
|
@ -506,7 +302,7 @@ fn setup_options_for_slot(
|
||||||
if aura_active {
|
if aura_active {
|
||||||
slot.auras.retain(|a| *a != aura);
|
slot.auras.retain(|a| *a != aura);
|
||||||
} else {
|
} else {
|
||||||
slot.auras.push(aura);
|
slot.auras = aura.try_add_to_list(&slot.auras);
|
||||||
}
|
}
|
||||||
update.emit(SettingSlotAction::Update(slot))
|
update.emit(SettingSlotAction::Update(slot))
|
||||||
})
|
})
|
||||||
|
|
@ -547,7 +343,7 @@ fn setup_options_for_slot(
|
||||||
state={open_aura_assign}
|
state={open_aura_assign}
|
||||||
options={options}
|
options={options}
|
||||||
>
|
>
|
||||||
{"auras"}
|
{slot.auras.len()}{" auras"}
|
||||||
</ClickableField>
|
</ClickableField>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -597,10 +393,10 @@ fn setup_options_for_slot(
|
||||||
let decrement_disabled_reason = prev.is_none().then_some("at minimum");
|
let decrement_disabled_reason = prev.is_none().then_some("at minimum");
|
||||||
Some(html! {
|
Some(html! {
|
||||||
<>
|
<>
|
||||||
<label>{"recruits"}</label>
|
<span>{"recruits"}</span>
|
||||||
<div class={classes!("increment-decrement")}>
|
<div class={classes!("increment-decrement")}>
|
||||||
<Button on_click={on_decrement} disabled_reason={decrement_disabled_reason}>{"-"}</Button>
|
<Button on_click={on_decrement} disabled_reason={decrement_disabled_reason}>{"-"}</Button>
|
||||||
<label>{recruits_available.get().to_string()}</label>
|
<span>{recruits_available.get().to_string()}</span>
|
||||||
<Button on_click={on_increment}>{"+"}</Button>
|
<Button on_click={on_increment}>{"+"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
@ -630,9 +426,9 @@ fn setup_options_for_slot(
|
||||||
};
|
};
|
||||||
Some(html! {
|
Some(html! {
|
||||||
<>
|
<>
|
||||||
<label>{"redeemed?"}</label>
|
<span>{"redeemed?"}</span>
|
||||||
<Button on_click={on_click}>
|
<Button on_click={on_click}>
|
||||||
<label>{body}</label>
|
<span>{body}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
})
|
})
|
||||||
|
|
@ -751,10 +547,10 @@ fn setup_options_for_slot(
|
||||||
});
|
});
|
||||||
Some(html! {
|
Some(html! {
|
||||||
<>
|
<>
|
||||||
<label>{"knows on night"}</label>
|
<span>{"knows on night"}</span>
|
||||||
<div class={classes!("increment-decrement")}>
|
<div class={classes!("increment-decrement")}>
|
||||||
<Button on_click={decrement}>{"-"}</Button>
|
<Button on_click={decrement}>{"-"}</Button>
|
||||||
<label>{knows_on_night.to_string()}</label>
|
<span>{knows_on_night.to_string()}</span>
|
||||||
<Button on_click={increment}>{"+"}</Button>
|
<Button on_click={increment}>{"+"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
@ -31,6 +31,9 @@ mod components {
|
||||||
pub mod action {
|
pub mod action {
|
||||||
werewolves_macros::include_path!("werewolves/src/components/action");
|
werewolves_macros::include_path!("werewolves/src/components/action");
|
||||||
}
|
}
|
||||||
|
pub mod settings {
|
||||||
|
werewolves_macros::include_path!("werewolves/src/components/settings");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mod pages {
|
mod pages {
|
||||||
werewolves_macros::include_path!("werewolves/src/pages");
|
werewolves_macros::include_path!("werewolves/src/pages");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue