night prompts/actions
This commit is contained in:
parent
d24ed6d86f
commit
80859f58d0
|
|
@ -7,10 +7,20 @@
|
|||
left: 0;
|
||||
user-select: none;
|
||||
|
||||
font-size: 3em;
|
||||
|
||||
.target-picker {
|
||||
font-size: 1.25em;
|
||||
|
||||
.target {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.role-reveal {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 2em;
|
||||
// font-size: 2em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
.icon-fit {
|
||||
height: 1em;
|
||||
// height: 1em;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
padding: 1ch;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
|
|
|||
|
|
@ -53,10 +53,13 @@ $starts_as_villager_color_faint: color.change($starts_as_villager_color, $alpha:
|
|||
$damned_color_faint: color.change($damned_color, $alpha: 0.1);
|
||||
$drunk_color_faint: color.change($drunk_color, $alpha: 0.1);
|
||||
|
||||
$wakes_color: oklch(0.9195 0.1839 109.60356514768961);
|
||||
|
||||
@import 'faction';
|
||||
@import 'setup';
|
||||
@import 'icon';
|
||||
@import 'big-screen';
|
||||
@import 'night';
|
||||
|
||||
@mixin flexbox() {
|
||||
display: -webkit-box;
|
||||
|
|
@ -918,3 +921,17 @@ form {
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.role-title-span {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
margin: 0 1ch 0 1ch;
|
||||
padding: 0.25ch 1ch 0.25ch 1ch;
|
||||
gap: 1ch;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
.cover-of-darkness {
|
||||
font-size: 3em;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
text-wrap: wrap;
|
||||
|
||||
p {
|
||||
padding: 3ch;
|
||||
}
|
||||
|
||||
& button {
|
||||
width: fit-content;
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.wolves-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.information {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
gap: 0.5ch;
|
||||
|
||||
font-size: 1.75em;
|
||||
height: 100%;
|
||||
|
||||
.subtext {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.arcanist-targets {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 1ch;
|
||||
font-size: 0.7em;
|
||||
align-items: center;
|
||||
|
||||
.and {
|
||||
font-style: italic;
|
||||
opacity: 50%;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.role-page {
|
||||
padding: 1vh 1vw 1vh 1vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
gap: 1ch;
|
||||
height: 98%;
|
||||
|
||||
.title {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.character {
|
||||
padding: 1ch;
|
||||
}
|
||||
}
|
||||
|
||||
.yellow {
|
||||
color: $wakes_color;
|
||||
}
|
||||
|
||||
.wolves-list {
|
||||
padding: 1ch;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
|
||||
.character {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 37vw;
|
||||
|
||||
font-size: 1.5em;
|
||||
|
||||
.role {
|
||||
font-size: 1.25em;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.continue-button {
|
||||
font-size: 2.25em;
|
||||
padding: 0.3ch;
|
||||
margin: 1ch;
|
||||
}
|
||||
|
||||
|
||||
.breakable {
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
.inline-icons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5ch;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.target-picker {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
height: 100%;
|
||||
font-size: 2em;
|
||||
|
||||
.target {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background-color: color.change($red1, $alpha: 0.1);
|
||||
border: 1px solid color.change($red1, $alpha: 0.6);
|
||||
|
||||
&.marked {
|
||||
background-color: color.change($blue1, $alpha: 0.3);
|
||||
border: 1px solid $blue1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.seer-icons,
|
||||
.arcanist-icons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
gap: 10%;
|
||||
}
|
||||
|
||||
.two-column {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 2fr;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.seer-check {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.false-positives {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
font-weight: bold;
|
||||
font-size: 0.5em;
|
||||
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bottom-bound {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
|
@ -68,7 +68,6 @@
|
|||
}
|
||||
|
||||
.wakes {
|
||||
$wakes_color: oklch(0.9195 0.1839 109.60356514768961);
|
||||
border: 2px solid $wakes_color;
|
||||
box-shadow: 0 0 3px $wakes_color;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ impl ActionPrompt {
|
|||
| ActionPrompt::Insomniac { .. } => true,
|
||||
}
|
||||
}
|
||||
pub(crate) const fn marked(&self) -> Option<(CharacterId, Option<CharacterId>)> {
|
||||
pub const fn marked(&self) -> Option<(CharacterId, Option<CharacterId>)> {
|
||||
match self {
|
||||
ActionPrompt::Seer { marked, .. }
|
||||
| ActionPrompt::Protector { marked, .. }
|
||||
|
|
@ -581,6 +581,46 @@ impl ActionPrompt {
|
|||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub fn targets(&self) -> Option<&[CharacterIdentity]> {
|
||||
match self {
|
||||
ActionPrompt::Seer { living_players: targets,.. }
|
||||
| ActionPrompt::Protector { targets,.. }
|
||||
| ActionPrompt::Arcanist { living_players: targets,.. }
|
||||
| ActionPrompt::Gravedigger { dead_players: targets,.. }
|
||||
| ActionPrompt::Hunter { living_players: targets,.. }
|
||||
| ActionPrompt::Militia { living_players: targets,.. }
|
||||
| ActionPrompt::MapleWolf { living_players: targets,.. }
|
||||
| ActionPrompt::Guardian { living_players: targets,.. }
|
||||
| ActionPrompt::Adjudicator { living_players: targets,.. }
|
||||
| ActionPrompt::PowerSeer { living_players: targets,.. }
|
||||
| ActionPrompt::Mortician { dead_players: targets,.. }
|
||||
| ActionPrompt::BeholderChooses { living_players: targets,.. }
|
||||
| ActionPrompt::MasonLeaderRecruit { potential_recruits: targets,.. }
|
||||
| ActionPrompt::Empath { living_players: targets,.. }
|
||||
| ActionPrompt::Vindicator { living_players: targets,.. }
|
||||
| ActionPrompt::PyreMaster { living_players: targets,.. }
|
||||
| ActionPrompt::WolfPackKill { living_villagers: targets,.. }
|
||||
| ActionPrompt::AlphaWolf { living_villagers: targets,.. }
|
||||
| ActionPrompt::DireWolf { living_players: targets,.. }
|
||||
| ActionPrompt::LoneWolfKill { living_players: targets, .. }
|
||||
| ActionPrompt::Bloodletter {
|
||||
living_players: targets,
|
||||
..
|
||||
} => Some(&**targets),
|
||||
|
||||
ActionPrompt::WolvesIntro { .. }
|
||||
| ActionPrompt::RoleChange { .. }
|
||||
| ActionPrompt::Shapeshifter { .. }
|
||||
| ActionPrompt::ElderReveal { .. }
|
||||
| ActionPrompt::Insomniac { .. }
|
||||
| ActionPrompt::CoverOfDarkness
|
||||
| ActionPrompt::MasonsWake { .. }
|
||||
| ActionPrompt::BeholderWakes { .. }
|
||||
| ActionPrompt::DamnedIntro { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for ActionPrompt {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
pub mod pages {
|
||||
werewolves_macros::include_path!("werewolves/src/app/pages");
|
||||
|
||||
pub mod night_actions {
|
||||
werewolves_macros::include_path!("werewolves/src/app/pages/night_actions");
|
||||
|
||||
pub mod role {
|
||||
werewolves_macros::include_path!("werewolves/src/app/pages/night_actions/role");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod components {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
use leptos::prelude::*;
|
||||
use werewolves_proto::message::{host::HostNightMessage, night::ActionResponse};
|
||||
|
||||
#[component]
|
||||
pub fn Cover(
|
||||
#[prop(optional)] message: &'static str,
|
||||
#[prop(optional)] reply: Option<WriteSignal<Option<HostNightMessage>>>,
|
||||
#[prop(default=HostNightMessage::ActionResponse(ActionResponse::Continue))]
|
||||
reply_to_send: HostNightMessage,
|
||||
) -> impl IntoView {
|
||||
let message = if message.is_empty() {
|
||||
"go to sleep"
|
||||
} else {
|
||||
message
|
||||
};
|
||||
let next = move || {
|
||||
reply.map(|reply| {
|
||||
let reply_to_send = reply_to_send.clone();
|
||||
view! { <button on:click=move |_| { reply.set(Some(reply_to_send.clone())) }>"continue"</button> }
|
||||
})
|
||||
};
|
||||
move || {
|
||||
view! {
|
||||
<div class="cover-of-darkness">
|
||||
<p>{message}</p>
|
||||
{next.clone()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,26 +2,20 @@ use leptos::prelude::*;
|
|||
use werewolves_proto::message::{Identification, PublicIdentity};
|
||||
|
||||
#[component]
|
||||
pub fn IdentityInline(ident: ReadSignal<PublicIdentity>) -> impl IntoView {
|
||||
let number = move || {
|
||||
ident
|
||||
.read()
|
||||
pub fn IdentityInline(ident: PublicIdentity) -> impl IntoView {
|
||||
let number = ident
|
||||
.number
|
||||
.as_ref()
|
||||
.map(|num| view! { <span class="number">{num.get()}</span> }.into_any())
|
||||
.unwrap_or_else(|| {
|
||||
view! { <span class="number red">"?"</span> }
|
||||
.into_any()
|
||||
})
|
||||
};
|
||||
.unwrap_or_else(|| view! { <span class="number red">"?"</span> }.into_any());
|
||||
let pronouns = move || {
|
||||
ident.read().pronouns.as_ref().map(|p| {
|
||||
ident.pronouns.as_ref().map(|p| {
|
||||
view! { <span class="pronouns">"("{p.clone()}")"</span> }
|
||||
})
|
||||
};
|
||||
view! {
|
||||
<span class="identity">
|
||||
{number} <span class="name">{move || ident.read().name.clone()}</span> {pronouns}
|
||||
{number} <span class="name">{move || ident.name.clone()}</span> {pronouns}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
|
@ -29,8 +23,7 @@ pub fn IdentityInline(ident: ReadSignal<PublicIdentity>) -> impl IntoView {
|
|||
#[component]
|
||||
pub fn IdentificationInline(ident: Identification) -> impl IntoView {
|
||||
if !ident.public.name.trim().is_empty() {
|
||||
return view! { <IdentityInline ident=RwSignal::new(ident.public).read_only() /> }
|
||||
.into_any();
|
||||
return view! { <IdentityInline ident=ident.public /> }.into_any();
|
||||
}
|
||||
view! {
|
||||
<span class="identity">
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ pub fn DialogModal(
|
|||
DialogMode::Close => view! { <div class="options">{close}</div> }.into_any(),
|
||||
DialogMode::ConfirmOrClose(on_confirm) => view! {
|
||||
<div class="options">
|
||||
<button on:click=move |_| on_confirm.run(())>{"confirm"}</button>
|
||||
<button on:click=move |_| on_confirm.run(())>confirm</button>
|
||||
{close}
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (C) 2026 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 leptos::prelude::*;
|
||||
use werewolves_proto::role::RoleTitle;
|
||||
|
||||
use crate::app::{
|
||||
class::{AsClasses, Class},
|
||||
components::{AssociatedIcon, Icon, IconType, PartialAssociatedIcon},
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn RoleSpan(role: RoleTitle) -> impl IntoView {
|
||||
let class = role.category().class();
|
||||
let icon = role
|
||||
.icon()
|
||||
.map(|icon| view! { <Icon source=icon r#type=IconType::Small /> }.into_any())
|
||||
.unwrap_or(view! { <div class="icon-spacer" /> }.into_any());
|
||||
let role_name = role.to_string().to_case(Case::Title);
|
||||
view! {
|
||||
<span class=["role-span", "faint", class]
|
||||
.as_classes()>{icon} <span class="role-name">{role_name}</span></span>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RoleTitleSpan(role: RoleTitle) -> impl IntoView {
|
||||
let class = role.category().class();
|
||||
let icon = role.icon().unwrap_or(role.alignment().icon());
|
||||
let text = role.to_string().to_case(Case::Title);
|
||||
view! {
|
||||
<span class=["role-title-span", "faint", class, "box"].as_classes()>
|
||||
<Icon source=icon r#type=IconType::Small />
|
||||
<span>{text}</span>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
|
@ -83,5 +83,5 @@ pub fn Sample(children: Children) -> impl IntoView {
|
|||
|
||||
#[component]
|
||||
pub fn Equals() -> impl IntoView {
|
||||
view! { <span class="equals">{"="}</span> }
|
||||
view! { <span class="equals">=</span> }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,4 +17,6 @@ pub enum WolfError {
|
|||
PasswordConfirmNoMatch,
|
||||
#[error("please set a seat number")]
|
||||
NoSeatNumber,
|
||||
#[error("no targets?")]
|
||||
NoTargets,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ use crate::{
|
|||
ConsoleLogError,
|
||||
app::{
|
||||
error::WolfError,
|
||||
pages::{NotFound, game::host::RoleRevealCharacter},
|
||||
pages::{
|
||||
NotFound,
|
||||
game::host::RoleRevealCharacter,
|
||||
night_actions::{RolePrompt, RoleResult},
|
||||
},
|
||||
storage::user::AuthContextStoreFields,
|
||||
},
|
||||
};
|
||||
|
|
@ -19,18 +23,30 @@ use leptos_use::{
|
|||
use reactive_stores::Store;
|
||||
use werewolves_proto::{
|
||||
game::GameId,
|
||||
message::{IntoClientResponse, WrappedServerMessage, host::ServerToHostMessage},
|
||||
message::{
|
||||
CharacterIdentity, IntoClientResponse, WrappedServerMessage,
|
||||
host::ServerToHostMessage,
|
||||
night::{ActionPrompt, ActionResult},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::app::storage::user::AuthContext;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
enum BigScreenPage {
|
||||
#[default]
|
||||
None,
|
||||
Setup,
|
||||
RoleReveal,
|
||||
QrCode,
|
||||
ActionPrompt {
|
||||
prompt: ActionPrompt,
|
||||
page: usize,
|
||||
},
|
||||
ActionResult {
|
||||
character: Option<CharacterIdentity>,
|
||||
result: ActionResult,
|
||||
},
|
||||
}
|
||||
|
||||
#[component]
|
||||
|
|
@ -155,8 +171,15 @@ pub fn BigScreen() -> impl IntoView {
|
|||
settings,
|
||||
} => todo!(),
|
||||
ServerToHostMessage::PlayerStates(_) => {}
|
||||
ServerToHostMessage::ActionPrompt(action_prompt, _) => todo!(),
|
||||
ServerToHostMessage::ActionResult(character_identity, action_result) => todo!(),
|
||||
ServerToHostMessage::ActionPrompt(act, ppage) => {
|
||||
page.set(BigScreenPage::ActionPrompt {
|
||||
prompt: act,
|
||||
page: ppage,
|
||||
});
|
||||
}
|
||||
ServerToHostMessage::ActionResult(character, result) => {
|
||||
page.set(BigScreenPage::ActionResult { character, result })
|
||||
}
|
||||
ServerToHostMessage::Lobby {
|
||||
players: p,
|
||||
settings: s,
|
||||
|
|
@ -199,9 +222,15 @@ pub fn BigScreen() -> impl IntoView {
|
|||
BigScreenPage::Setup => {
|
||||
view! { <SetupView settings=settings.read_only() /> }.into_any()
|
||||
}
|
||||
BigScreenPage::RoleReveal => view! { <BigScreenRoleReveal acks=acks.read_only() /> }
|
||||
.into_any(),
|
||||
BigScreenPage::RoleReveal => {
|
||||
view! { <BigScreenRoleReveal acks=acks.read_only() /> }.into_any()
|
||||
}
|
||||
BigScreenPage::QrCode => view! { <QrView game_id=game_id /> }.into_any(),
|
||||
BigScreenPage::ActionPrompt { prompt, page } => {
|
||||
view! { <RolePrompt prompt=prompt page=page /> }.into_any()
|
||||
}
|
||||
BigScreenPage::ActionResult { character, result } => view! { <RoleResult character=character result=result /> }
|
||||
.into_any(),
|
||||
};
|
||||
|
||||
view! { <div class="big-screen-wrapper">{content}</div> }.into_any()
|
||||
|
|
|
|||
|
|
@ -9,10 +9,9 @@ pub fn BigScreenRoleReveal(acks: ReadSignal<Box<[RoleRevealCharacter]>>) -> impl
|
|||
.into_iter()
|
||||
.map(|ackd| {
|
||||
let RoleRevealCharacter { char, acknowledged } = ackd;
|
||||
let ident = RwSignal::new(char.into_public()).read_only();
|
||||
view! {
|
||||
<div class="player" class:ready=acknowledged>
|
||||
<IdentityInline ident=ident />
|
||||
<IdentityInline ident=char.into_public() />
|
||||
</div>
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ pub fn QrView(game_id: GameId) -> impl IntoView {
|
|||
<div class="qrcode">
|
||||
<img src=qrcode_url alt="qr code to join" />
|
||||
<div class="details">
|
||||
<h3>{"scan the qrcode to join"}</h3>
|
||||
<h3>scan the qrcode to join</h3>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ pub fn SetupView(settings: ReadSignal<GameSettings>) -> impl IntoView {
|
|||
{categories} <div class="setup-category big">
|
||||
<span class="slot">
|
||||
<span class="count">{power_roles_count}</span>
|
||||
<span class="title village box">{"Power roles from..."}</span>
|
||||
<span class="title village box">Power roles from...</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,20 +6,36 @@ use leptos::prelude::*;
|
|||
use werewolves_proto::{
|
||||
game::{Category, GameSettings},
|
||||
message::{
|
||||
PlayerState,
|
||||
host::{HostLobbyMessage, HostMessage, ServerToHostMessage as Srv2Host},
|
||||
CharacterIdentity, PlayerState,
|
||||
host::{
|
||||
HostGameMessage, HostLobbyMessage, HostMessage, HostNightMessage,
|
||||
ServerToHostMessage as Srv2Host,
|
||||
},
|
||||
night::{ActionPrompt, ActionResult},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::app::{Preferences, components::DialogModal};
|
||||
use crate::app::{
|
||||
Preferences,
|
||||
components::DialogModal,
|
||||
pages::night_actions::{RolePrompt, RoleResult},
|
||||
};
|
||||
use crate::{ConsoleLogError, app::error::WolfError};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
enum HostPage {
|
||||
#[default]
|
||||
None,
|
||||
Settings,
|
||||
RoleRevealAcks,
|
||||
ActionPrompt {
|
||||
prompt: ActionPrompt,
|
||||
page: usize,
|
||||
},
|
||||
ActionResult {
|
||||
character: Option<CharacterIdentity>,
|
||||
result: ActionResult,
|
||||
},
|
||||
}
|
||||
|
||||
#[component]
|
||||
|
|
@ -93,6 +109,15 @@ pub fn HostGamePage(
|
|||
acks.set(reveals);
|
||||
page.set(HostPage::RoleRevealAcks);
|
||||
}
|
||||
Srv2Host::ActionPrompt(prompt, prompt_page) => {
|
||||
page.set(HostPage::ActionPrompt {
|
||||
prompt,
|
||||
page: prompt_page,
|
||||
});
|
||||
}
|
||||
Srv2Host::ActionResult(character, result) => {
|
||||
page.set(HostPage::ActionResult { character, result })
|
||||
}
|
||||
_ => log::error!("{message:#?}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -120,6 +145,26 @@ pub fn HostGamePage(
|
|||
HostPage::RoleRevealAcks => {
|
||||
view! { <RoleRevealAcks reply=reply error=error acks=acks.read_only() /> }.into_any()
|
||||
}
|
||||
HostPage::ActionPrompt { prompt, page } => {
|
||||
let reply_prompt = RwSignal::new(None);
|
||||
Effect::new(move || {
|
||||
if let Some(r) = reply_prompt.get() {
|
||||
reply.set(Some(HostMessage::InGame(HostGameMessage::Night(r))))
|
||||
}
|
||||
});
|
||||
view! { <RolePrompt reply=reply_prompt.write_only() prompt=prompt page=page /> }
|
||||
.into_any()
|
||||
}
|
||||
HostPage::ActionResult { character, result } => {
|
||||
let reply_result = RwSignal::new(None);
|
||||
Effect::new(move || {
|
||||
if let Some(r) = reply_result.get() {
|
||||
reply.set(Some(HostMessage::InGame(HostGameMessage::Night(r))))
|
||||
}
|
||||
});
|
||||
view! { <RoleResult reply=reply_result.write_only() character=character result=result /> }
|
||||
.into_any()
|
||||
}
|
||||
};
|
||||
view! {
|
||||
{cancel}
|
||||
|
|
|
|||
|
|
@ -23,14 +23,13 @@ pub fn RoleRevealAcks(
|
|||
.map(|ackd| {
|
||||
let RoleRevealCharacter { char, acknowledged } = ackd;
|
||||
let char_id = char.character_id;
|
||||
let ident = RwSignal::new(char.into_public()).read_only();
|
||||
let force_ready = move |ev: MouseEvent| {
|
||||
ev.prevent_default();
|
||||
reply.set(Some(HostMessage::ForceRoleAckFor(char_id)));
|
||||
};
|
||||
view! {
|
||||
<div class="player" class:ready=acknowledged>
|
||||
<IdentityInline ident=ident />
|
||||
<IdentityInline ident=char.into_public() />
|
||||
<button disabled=acknowledged on:click=force_ready>
|
||||
"force ready"
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -236,15 +236,12 @@ fn SettingsSetupSlot(
|
|||
.iter()
|
||||
.find(|p| p.identification.player_id == a)
|
||||
{
|
||||
Some(player) => {
|
||||
let ident = RwSignal::new(player.identification.public.clone());
|
||||
view! {
|
||||
Some(player) => view! {
|
||||
<span class="assignment">
|
||||
<IdentityInline ident=ident.read_only() />
|
||||
<IdentityInline ident=player.identification.public.clone() />
|
||||
</span>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
.into_any(),
|
||||
None => view! { <span class="missing error">"assigned player not in lobby"</span> }
|
||||
.into_any(),
|
||||
}
|
||||
|
|
@ -335,7 +332,7 @@ fn SlotSettingsDialogBody(
|
|||
.cloned()
|
||||
})
|
||||
.map(|p| p.identification.public)
|
||||
.map(|id| view! { <IdentityInline ident=RwSignal::new(id).read_only() /> }.into_any())
|
||||
.map(|id| view! { <IdentityInline ident=id /> }.into_any())
|
||||
.unwrap_or_else(|| view! { "none" }.into_any());
|
||||
let remove = move |ev: MouseEvent| {
|
||||
ev.prevent_default();
|
||||
|
|
@ -429,7 +426,6 @@ fn AssignmentSelection(
|
|||
.as_ref()
|
||||
.map(|assigned_id| p.identification.player_id == *assigned_id)
|
||||
.unwrap_or_default();
|
||||
let ident = RwSignal::new(p.identification.public.clone());
|
||||
let pid = p.identification.player_id;
|
||||
let assign = move |ev: MouseEvent| {
|
||||
ev.prevent_default();
|
||||
|
|
@ -438,7 +434,7 @@ fn AssignmentSelection(
|
|||
|
||||
view! {
|
||||
<button on:click=assign class:selected=assigned class="player-select">
|
||||
<IdentityInline ident=ident.read_only() />
|
||||
<IdentityInline ident=p.identification.public.clone() />
|
||||
</button>
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
use leptos::prelude::*;
|
||||
|
||||
use crate::app::components::{Icon, IconSource};
|
||||
|
||||
#[component]
|
||||
pub fn DamnedIntroPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="damned box title">"DAMNED"</span>
|
||||
<div class="information box damned faint">
|
||||
<span class="yellow">"YOU ARE DAMNED"</span>
|
||||
<Icon source=IconSource::Damned />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn DamnedIntroPage2() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="damned box title">"DAMNED"</span>
|
||||
<div class="information box damned faint">
|
||||
"YOU RETAIN YOUR ROLE AND WIN IF EVIL WINS"
|
||||
<span class="yellow">"HOWEVER, YOU CONTRIBUTE TO VILLAGE PARITY"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
use leptos::prelude::*;
|
||||
use werewolves_proto::aura::AuraTitle;
|
||||
|
||||
use crate::app::components::{Icon, IconSource, PartialAssociatedIcon};
|
||||
|
||||
#[component]
|
||||
pub fn DrunkPage() -> impl IntoView {
|
||||
let icon = AuraTitle::Drunk.icon().unwrap_or(IconSource::Roleblock);
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="drunk box title">"DRUNK"</span>
|
||||
<div class="information box drunk faint">
|
||||
"YOU GOT DRUNK INSTEAD" <Icon source=icon />
|
||||
<span class="yellow">"YOUR NIGHT ACTION DID NOT TAKE PLACE"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
use leptos::prelude::*;
|
||||
use werewolves_proto::{
|
||||
character::CharacterId,
|
||||
message::{
|
||||
CharacterIdentity,
|
||||
host::HostNightMessage,
|
||||
night::{ActionPrompt, ActionResponse},
|
||||
},
|
||||
role::PreviousGuardianAction,
|
||||
};
|
||||
|
||||
use crate::app::{
|
||||
class::AsClasses,
|
||||
components::{Cover, IdentityInline},
|
||||
error::WolfError,
|
||||
pages::night_actions::{
|
||||
DamnedIntroPage1, DamnedIntroPage2, RoleChange, WolfPackKill, WolvesIntro,
|
||||
role::{
|
||||
AdjudicatorPage1, AlphaWolfPage1, ArcanistPage1, BeholderPage1, BeholderWakePage1,
|
||||
BloodletterPage1, DirewolfPage1, ElderPage1, ElderPage2, EmpathPage1, GravediggerPage1,
|
||||
GuardianPageNoPrevProtect, GuardianPagePreviousGuard, GuardianPagePreviousProtect1,
|
||||
GuardianPagePreviousProtect2, HunterPage1, InsomniacPage1, LoneWolfPage1,
|
||||
MapleWolfPage1, MasonRecruitPage1, MasonRecruitPage2, MasonsWake, MilitiaPage1,
|
||||
MorticianPage1, PowerSeerPage1, ProtectorPage1, PyremasterPage1, SeerPage1,
|
||||
VindicatorPage1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
pub trait RolePage {
|
||||
fn role_pages(&self, big_screen: bool) -> ViewFn;
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RolePrompt(
|
||||
prompt: ActionPrompt,
|
||||
page: usize,
|
||||
#[prop(optional)] reply: Option<WriteSignal<Option<HostNightMessage>>>,
|
||||
#[prop(optional)] error: Option<WriteSignal<Option<WolfError>>>,
|
||||
) -> impl IntoView {
|
||||
let ident = move |character_id: CharacterIdentity| {
|
||||
reply.map(|_| {
|
||||
view! { <IdentityInline ident=character_id.into() /> }
|
||||
})
|
||||
};
|
||||
let interactive = prompt.interactive();
|
||||
let targets = prompt.targets().map(|t| t.to_vec().into_boxed_slice());
|
||||
let marked = prompt
|
||||
.marked()
|
||||
.map(|t| [t.0].into_iter().chain(t.1))
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Box<_>>();
|
||||
let mut pages: Vec<AnyView> = match prompt {
|
||||
ActionPrompt::CoverOfDarkness => vec![match reply {
|
||||
Some(reply) => view! { <Cover reply=reply /> }.into_any(),
|
||||
None => view! { <Cover /> }.into_any(),
|
||||
}],
|
||||
ActionPrompt::ElderReveal { character_id } => vec![
|
||||
view! {
|
||||
{ident(character_id.clone())}
|
||||
<ElderPage1 />
|
||||
}
|
||||
.into_any(),
|
||||
view! {
|
||||
{ident(character_id)}
|
||||
<ElderPage2 />
|
||||
}
|
||||
.into_any(),
|
||||
],
|
||||
ActionPrompt::Adjudicator { character_id, .. } => vec![
|
||||
view! {
|
||||
{ident(character_id)}
|
||||
<AdjudicatorPage1 />
|
||||
}
|
||||
.into_any(),
|
||||
],
|
||||
ActionPrompt::RoleChange {
|
||||
character_id,
|
||||
new_role,
|
||||
} => vec![
|
||||
view! {
|
||||
{ident(character_id)}
|
||||
<RoleChange role=new_role />
|
||||
}
|
||||
.into_any(),
|
||||
],
|
||||
ActionPrompt::Seer { character_id, .. } => vec![
|
||||
view! {
|
||||
{ident(character_id)}
|
||||
<SeerPage1 />
|
||||
}
|
||||
.into_any(),
|
||||
],
|
||||
ActionPrompt::Protector { character_id, .. } => vec![
|
||||
view! {
|
||||
{ident(character_id)}
|
||||
<ProtectorPage1 />
|
||||
}
|
||||
.into_any(),
|
||||
],
|
||||
ActionPrompt::Arcanist { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <ArcanistPage1 /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::Gravedigger { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <GravediggerPage1 /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::Hunter { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <HunterPage1 /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::Militia { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <MilitiaPage1 /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::MapleWolf {
|
||||
character_id,
|
||||
nights_til_starvation,
|
||||
..
|
||||
} => {
|
||||
vec![view! { {ident(character_id)} <MapleWolfPage1 nights_til_starvation=nights_til_starvation/>}.into_any()]
|
||||
}
|
||||
ActionPrompt::Guardian {
|
||||
character_id,
|
||||
previous: None,
|
||||
..
|
||||
} => {
|
||||
vec![view! { {ident(character_id)} <GuardianPageNoPrevProtect /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::Guardian {
|
||||
character_id,
|
||||
previous: Some(PreviousGuardianAction::Protect(previous)),
|
||||
..
|
||||
} => {
|
||||
vec![
|
||||
view! { {ident(character_id.clone())} <GuardianPagePreviousProtect1 previous=previous.clone() /> }
|
||||
.into_any(),
|
||||
view! { {ident(character_id)} <GuardianPagePreviousProtect2 previous=previous /> }
|
||||
.into_any(),
|
||||
]
|
||||
}
|
||||
ActionPrompt::Guardian {
|
||||
character_id,
|
||||
previous: Some(PreviousGuardianAction::Guard(previous)),
|
||||
..
|
||||
} => {
|
||||
vec![
|
||||
view! { {ident(character_id)} <GuardianPagePreviousGuard previous=previous /> }
|
||||
.into_any(),
|
||||
]
|
||||
}
|
||||
ActionPrompt::PowerSeer { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <PowerSeerPage1 />}.into_any()]
|
||||
}
|
||||
ActionPrompt::Mortician { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <MorticianPage1 /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::BeholderChooses { character_id, .. } => vec![
|
||||
view! {
|
||||
{ident(character_id)}
|
||||
<BeholderPage1 />
|
||||
}
|
||||
.into_any(),
|
||||
],
|
||||
ActionPrompt::MasonLeaderRecruit {
|
||||
character_id,
|
||||
recruits_left,
|
||||
..
|
||||
} => {
|
||||
vec![
|
||||
view! { {ident(character_id.clone())} <MasonRecruitPage1 recruits_left=recruits_left /> }
|
||||
.into_any(),
|
||||
view! { {ident(character_id)} <MasonRecruitPage2 /> }.into_any(),
|
||||
]
|
||||
}
|
||||
ActionPrompt::Empath { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <EmpathPage1 /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::Vindicator { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <VindicatorPage1 /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::PyreMaster { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <PyremasterPage1 /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::Shapeshifter { .. } => {
|
||||
vec![]
|
||||
}
|
||||
ActionPrompt::AlphaWolf { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <AlphaWolfPage1 /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::DireWolf { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <DirewolfPage1 /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::LoneWolfKill { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <LoneWolfPage1 /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::Insomniac { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <InsomniacPage1 /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::Bloodletter { character_id, .. } => {
|
||||
vec![view! { {ident(character_id)} <BloodletterPage1 /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::DamnedIntro { character_id, .. } => vec![
|
||||
view! {
|
||||
{ident(character_id.clone())}
|
||||
<DamnedIntroPage1 />
|
||||
}
|
||||
.into_any(),
|
||||
view! {
|
||||
{ident(character_id)}
|
||||
<DamnedIntroPage2 />
|
||||
}
|
||||
.into_any(),
|
||||
],
|
||||
ActionPrompt::BeholderWakes { character_id, .. } => vec![
|
||||
view! {
|
||||
{ident(character_id)}
|
||||
<BeholderWakePage1 />
|
||||
}
|
||||
.into_any(),
|
||||
],
|
||||
ActionPrompt::WolfPackKill { .. } => vec![view! { <WolfPackKill /> }.into_any()],
|
||||
ActionPrompt::MasonsWake { leader, masons } => {
|
||||
vec![view! { <MasonsWake leader=leader masons=masons /> }.into_any()]
|
||||
}
|
||||
ActionPrompt::WolvesIntro { wolves } => {
|
||||
vec![view! { <WolvesIntro wolves=wolves /> }.into_any()]
|
||||
}
|
||||
};
|
||||
// isn't it great that AnyView isn't Clone????
|
||||
if let Some(page) = pages.get(page).is_some().then(|| {
|
||||
let p = pages.swap_remove(page);
|
||||
let next_btn = reply.map(|reply| {
|
||||
let next = move |_| {
|
||||
reply.set(Some(if pages.get(page + 1).is_none() && !interactive {
|
||||
HostNightMessage::ActionResponse(ActionResponse::ContinueToResult)
|
||||
} else {
|
||||
HostNightMessage::NextPage
|
||||
}));
|
||||
};
|
||||
view! {
|
||||
<button on:click=next class="continue-button">
|
||||
"continue"
|
||||
</button>
|
||||
}
|
||||
});
|
||||
view! {
|
||||
{p}
|
||||
{next_btn}
|
||||
}
|
||||
}) {
|
||||
return page.into_any();
|
||||
}
|
||||
let target_picker = match targets {
|
||||
Some(targets) => {
|
||||
let pick = RwSignal::new(None);
|
||||
match reply {
|
||||
Some(reply) => {
|
||||
Effect::new(move || {
|
||||
if let Some(target) = pick.get() {
|
||||
reply.set(Some(HostNightMessage::ActionResponse(
|
||||
ActionResponse::MarkTarget(target),
|
||||
)));
|
||||
}
|
||||
});
|
||||
view! { <TargetPicker targets=targets marked=marked pick=pick.write_only() /> }
|
||||
.into_any()
|
||||
}
|
||||
None => view! { <TargetPicker targets=targets marked=marked /> }.into_any(),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(error) = error {
|
||||
error.set(Some(WolfError::NoTargets));
|
||||
} else {
|
||||
log::error!("no targets?");
|
||||
}
|
||||
().into_any()
|
||||
}
|
||||
};
|
||||
let continue_btn = reply.map(|reply| {
|
||||
view! {
|
||||
<button
|
||||
class="continue-button"
|
||||
on:click=move |_| {
|
||||
reply.set(Some(HostNightMessage::ActionResponse(ActionResponse::Continue)))
|
||||
}
|
||||
>
|
||||
"continue"
|
||||
</button>
|
||||
}
|
||||
});
|
||||
view! {
|
||||
{target_picker}
|
||||
{continue_btn}
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TargetPicker(
|
||||
targets: Box<[CharacterIdentity]>,
|
||||
#[allow(clippy::boxed_local)] marked: Box<[CharacterId]>,
|
||||
#[prop(optional)] pick: Option<WriteSignal<Option<CharacterId>>>,
|
||||
) -> impl IntoView {
|
||||
let targets = targets
|
||||
.into_iter()
|
||||
.map(|target| {
|
||||
let marked = marked.contains(&target.character_id);
|
||||
let char_id = target.character_id;
|
||||
let pick = move |_| {
|
||||
if let Some(pick) = pick {
|
||||
pick.set(Some(char_id));
|
||||
}
|
||||
};
|
||||
view! {
|
||||
<button
|
||||
class=["character", "target", "no-hover", "village", "faint"].as_classes()
|
||||
class:marked=marked
|
||||
on:click=pick
|
||||
>
|
||||
<IdentityInline ident=target.into_public() />
|
||||
</button>
|
||||
}
|
||||
})
|
||||
.collect_view();
|
||||
|
||||
view! { <div class="target-picker">{targets}</div> }
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
use leptos::prelude::*;
|
||||
use werewolves_proto::message::{
|
||||
CharacterIdentity,
|
||||
host::HostNightMessage,
|
||||
night::{ActionResponse, ActionResult},
|
||||
};
|
||||
|
||||
use crate::app::{
|
||||
components::Cover,
|
||||
pages::night_actions::{
|
||||
DrunkPage, RoleblockPage,
|
||||
role::{
|
||||
AdjudicatorResult, ArcanistResult, EmpathResult, GravediggerResultPage,
|
||||
InsomniacResult, MorticianResultPage, PowerSeerResult, SeerResult,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn RoleResult(
|
||||
character: Option<CharacterIdentity>,
|
||||
result: ActionResult,
|
||||
#[prop(optional)] reply: Option<WriteSignal<Option<HostNightMessage>>>,
|
||||
) -> impl IntoView {
|
||||
let body = match result {
|
||||
ActionResult::RoleBlocked => view! { <RoleblockPage /> }.into_any(),
|
||||
ActionResult::Drunk => view! { <DrunkPage /> }.into_any(),
|
||||
ActionResult::Seer(target, alignment) => view! { <SeerResult target=target.into_public() alignment=alignment /> }.into_any(),
|
||||
ActionResult::PowerSeer { target, powerful } => view! { <PowerSeerResult target=target.into_public() powerful=powerful /> }.into_any(),
|
||||
ActionResult::Adjudicator { target, killer } => view! { <AdjudicatorResult target=target.into_public() killer=killer /> }.into_any(),
|
||||
ActionResult::Arcanist((target1, target2), alignment_eq) => view! {
|
||||
<ArcanistResult
|
||||
value=alignment_eq
|
||||
targets=(target1.into_public(), target2.into_public())
|
||||
/>
|
||||
}.into_any(),
|
||||
ActionResult::GraveDigger(target, role) => view! { <GravediggerResultPage target=target.into_public() role=role /> }.into_any(),
|
||||
ActionResult::Mortician(target, died_to) => view! { <MorticianResultPage target=target.into_public() died_to=died_to /> }.into_any(),
|
||||
ActionResult::Insomniac(visits) => view! { <InsomniacResult visits=visits /> }.into_any(),
|
||||
ActionResult::Empath { target, scapegoat } => view! { <EmpathResult target=target.into_public() scapegoat=scapegoat /> }.into_any(),
|
||||
ActionResult::BeholderSawNothing => todo!(),
|
||||
ActionResult::BeholderSawEverything => todo!(),
|
||||
ActionResult::GoBackToSleep => return match reply {
|
||||
Some(reply) => view! { <Cover message="go to sleep" reply=reply reply_to_send=HostNightMessage::Next /> }
|
||||
.into_any(),
|
||||
None => view! { <Cover message="go to sleep" /> }.into_any(),
|
||||
},
|
||||
ActionResult::ShiftFailed => todo!(),
|
||||
ActionResult::Continue => {
|
||||
Effect::new(move || {
|
||||
let Some(reply) = reply else {
|
||||
return;
|
||||
};
|
||||
reply.set(Some(HostNightMessage::ActionResponse(
|
||||
ActionResponse::ContinueToResult,
|
||||
)));
|
||||
});
|
||||
().into_any()
|
||||
}
|
||||
ActionResult::SkippedByHost => todo!(),
|
||||
};
|
||||
let next_btn = reply.map(|reply| {
|
||||
view! {
|
||||
<button
|
||||
class="continue-button"
|
||||
on:click=move |_| {
|
||||
reply.set(Some(HostNightMessage::ActionResponse(ActionResponse::Continue)))
|
||||
}
|
||||
>
|
||||
"continue"
|
||||
</button>
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
{body}
|
||||
{next_btn}
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
use werewolves_proto::{message::PublicIdentity, role::Killer};
|
||||
|
||||
use crate::app::components::{Icon, IconSource, IconType, IdentityInline};
|
||||
|
||||
#[component]
|
||||
pub fn AdjudicatorPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="defensive box title">"ADJUDICATOR"</span>
|
||||
<div class="information box defensive faint">
|
||||
<h4>"PICK A PLAYER"</h4>
|
||||
<Icon source=IconSource::Killer r#type=IconType::Fit />
|
||||
<h4 class="yellow">"YOU WILL CHECK IF THEY APPEAR AS A KILLER"</h4>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn AdjudicatorResult(killer: Killer, target: PublicIdentity) -> impl IntoView {
|
||||
let text = match killer {
|
||||
Killer::Killer => "IS A KILLER",
|
||||
Killer::NotKiller => "IS NOT A KILLER",
|
||||
};
|
||||
let icon = match killer {
|
||||
Killer::Killer => view! { <Icon source=IconSource::Killer r#type=IconType::Fit /> },
|
||||
Killer::NotKiller => view! { <Icon source=IconSource::RedX r#type=IconType::Fit /> },
|
||||
};
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="defensive box title">"ADJUDICATOR"</span>
|
||||
<div class="information box defensive faint">
|
||||
<IdentityInline ident=target.clone() />
|
||||
{icon}
|
||||
<h3 class="yellow">{text}</h3>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn AlphaWolfPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="wolves box title">"ALPHA WOLF"</span>
|
||||
<div class="information box wolves faint">
|
||||
<span class="breakable">
|
||||
"IF YOU WISH TO USE YOUR " <span class="yellow">"ONCE PER GAME"</span>
|
||||
" KILL ABILITY"
|
||||
</span>
|
||||
"POINT AT YOUR TARGET OR GO BACK TO SLEEP"
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
use werewolves_proto::{message::PublicIdentity, role::AlignmentEq};
|
||||
|
||||
use crate::app::components::{Icon, IconSource, IdentityInline};
|
||||
|
||||
#[component]
|
||||
pub fn ArcanistPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"ARCANIST"</span>
|
||||
<div class="information box intel faint">
|
||||
// space
|
||||
"PICK TWO PLAYERS" <div class="arcanist-icons">
|
||||
<Icon source=IconSource::Village />
|
||||
<Icon source=IconSource::Wolves />
|
||||
</div> <span class="yellow">"YOU WILL COMPARE THEIR ALIGNMENTS"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ArcanistResult(
|
||||
value: AlignmentEq,
|
||||
targets: (PublicIdentity, PublicIdentity),
|
||||
) -> impl IntoView {
|
||||
let text = match value {
|
||||
AlignmentEq::Same => "ARE THE SAME",
|
||||
AlignmentEq::Different => "ARE DIFFERENT",
|
||||
};
|
||||
let icons = match value {
|
||||
AlignmentEq::Same => view! { <Icon source=IconSource::Equal /> },
|
||||
AlignmentEq::Different => view! { <Icon source=IconSource::NotEqual /> },
|
||||
};
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"ARCANIST"</span>
|
||||
<div class="information box intel faint">
|
||||
<div class="arcanist-targets">
|
||||
<IdentityInline ident=targets.0 />
|
||||
<span class="and">"AND"</span>
|
||||
<IdentityInline ident=targets.1 />
|
||||
</div>
|
||||
<div class="icons">{icons}</div>
|
||||
<span class="yellow">{text}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
|
||||
use crate::app::components::{Icon, IconSource};
|
||||
|
||||
#[component]
|
||||
pub fn BeholderPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"BEHOLDER"</span>
|
||||
<div class="information box intel faint">
|
||||
"PICK A PLAYER" <Icon source=IconSource::Beholder />
|
||||
"YOU WILL SEE WHAT INFORMATION THEY MAY HAVE GATHERED"
|
||||
<span class="yellow">"SHOULD THEY DIE TONIGHT"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BeholderWakePage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"BEHOLDER"</span>
|
||||
<div class="information box intel faint">
|
||||
"YOUR TARGET HAS DIED" <Icon source=IconSource::Beholder />
|
||||
<span class="yellow">"THIS IS THE LAST PIECE OF INFORMATION THEY SAW"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BeholderSawNothing() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"BEHOLDER"</span>
|
||||
<div class="information box intel faint">
|
||||
<h1>"YOUR TARGET HAS DIED"</h1>
|
||||
<Icon source=IconSource::RedX />
|
||||
<h1>"BUT SAW NOTHING"</h1>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BeholderSawEverything() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"BEHOLDER"</span>
|
||||
<div class="information box intel faint">
|
||||
"YOUR TARGET HAS DIED" <Icon source=IconSource::Beholder />
|
||||
<span>
|
||||
"BUT SAW " <em class="red">
|
||||
<strong>"EVERYTHING"</strong>
|
||||
</em>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
|
||||
use crate::app::components::{Icon, IconSource};
|
||||
|
||||
#[component]
|
||||
pub fn BloodletterPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="wolves box title">"BLOODLETTER"</span>
|
||||
<div class="information box wolves faint">
|
||||
<span>"PICK A PLAYER"</span>
|
||||
<span class="inline-icons">
|
||||
"THEY'LL APPEAR AS A WOLF " <Icon source=IconSource::Wolves /> "KILLER"
|
||||
<Icon source=IconSource::Killer /> "AND POWERFUL"
|
||||
<Icon source=IconSource::Powerful /> "IN CHECKS FOR 2 NIGHTS"
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
use leptos::IntoView;
|
||||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn DirewolfPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="wolves box title">"DIREWOLF"</span>
|
||||
<div class="information box wolves faint">
|
||||
"CHOOSE A TARGET"
|
||||
<span class="yellow">"ANY VISITORS TO THIS TARGET WILL BE ROLE BLOCKED"</span>
|
||||
"YOU CANNOT CHOOSE YOURSELF OR THE SAME TARGET AS LAST NIGHT"
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn ElderPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="starts-as-villager">"ELDER"</span>
|
||||
<div class="information box starts-as-villager faint">
|
||||
"YOU ARE THE ELDER"
|
||||
<span class="yellow">
|
||||
"IF YOU ARE EXECUTED BY THE VILLAGE FROM NOW ON " "ALL POWER ROLES WILL BE LOST"
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ElderPage2() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="starts-as-villager box title">"ELDER"</span>
|
||||
<div class="information box starts-as-villager faint">
|
||||
"YOU STARTED THE GAME WITH PROTECTION FROM A NIGHT "
|
||||
"DEATH — THIS MAY OR MAY NOT STILL BE INTACT"
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
use werewolves_proto::message::PublicIdentity;
|
||||
|
||||
use crate::app::components::{Icon, IconSource, IdentityInline};
|
||||
|
||||
#[component]
|
||||
pub fn EmpathPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"EMPATH"</span>
|
||||
<div class="information box intel faint">
|
||||
"PICK A PLAYER"
|
||||
<span class="yellow">"YOU WILL CHECK IF THEY ARE THE SCAPEGOAT"</span>
|
||||
"IF THEY ARE, YOU WILL TAKE ON THEIR CURSE"
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn EmpathResult(scapegoat: bool, target: PublicIdentity) -> impl IntoView {
|
||||
let text = match scapegoat {
|
||||
true => "IS THE SCAPEGOAT",
|
||||
false => "IS NOT THE SCAPEGOAT",
|
||||
};
|
||||
let icon = match scapegoat {
|
||||
true => IconSource::Scapegoat,
|
||||
false => IconSource::RedX,
|
||||
};
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"EMPATH"</span>
|
||||
<div class="information box intel faint">
|
||||
<IdentityInline ident=target />
|
||||
<Icon source=icon />
|
||||
<span class="yellow">{text}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
use werewolves_proto::{message::PublicIdentity, role::RoleTitle};
|
||||
|
||||
use crate::app::components::{Icon, IconSource, IdentityInline, PartialAssociatedIcon};
|
||||
|
||||
#[component]
|
||||
pub fn GravediggerPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"GRAVEDIGGER"</span>
|
||||
<div class="information box intel faint">
|
||||
<span>"PICK A " <span class="yellow">"DEAD"</span> " PLAYER"</span>
|
||||
<Icon source=IconSource::Gravedigger />
|
||||
<span class="yellow">"YOU WILL LEARN THEIR ROLE"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn GravediggerResultPage(role: Option<RoleTitle>, target: PublicIdentity) -> impl IntoView {
|
||||
let text = role
|
||||
.as_ref()
|
||||
.map(|r| {
|
||||
view! {
|
||||
"WAS A "
|
||||
<span class="yellow">{r.to_string().to_case(Case::Upper)}</span>
|
||||
}
|
||||
.into_any()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
view! { "YOU FIND AN EMPTY GRAVE" }
|
||||
.into_any()
|
||||
});
|
||||
let icon = role
|
||||
.as_ref()
|
||||
.and_then(|i| i.icon())
|
||||
.unwrap_or(IconSource::Gravedigger);
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"GRAVEDIGGER"</span>
|
||||
<div class="information box intel faint">
|
||||
<IdentityInline ident=target />
|
||||
<Icon source=icon />
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
use werewolves_proto::message::CharacterIdentity;
|
||||
|
||||
use crate::app::components::{Icon, IconSource, IdentityInline};
|
||||
|
||||
#[component]
|
||||
pub fn GuardianPageNoPrevProtect() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="defensive box title">"GUARDIAN"</span>
|
||||
<div class="information box defensive faint">
|
||||
"PICK A PLAYER" <Icon source=IconSource::ShieldAndSword />
|
||||
<span class="yellow">"CHOOSE SOMEONE TO PROTECT FROM DEATH"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn GuardianPagePreviousProtectSelf() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="defensive box title">"GUARDIAN"</span>
|
||||
<div class="information box defensive faint">
|
||||
"LAST TIME YOU PROTECTED YOURSELF"
|
||||
<span class="yellow">"YOU CANNOT PROTECT YOURSELF AGAIN TONIGHT"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn GuardianPagePreviousProtect1(previous: CharacterIdentity) -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="defensive box title">"GUARDIAN"</span>
|
||||
<div class="information box defensive faint">
|
||||
"LAST TIME YOU PROTECTED" <Icon source=IconSource::ShieldAndSword />
|
||||
<div class="info-player-list">
|
||||
<IdentityInline ident=previous.into_public() />
|
||||
</div>
|
||||
<span class="yellow">"IF YOU PROTECT THEM AGAIN, YOU WILL INSTEAD GUARD THEM"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn GuardianPagePreviousProtect2(previous: CharacterIdentity) -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="defensive box title">"GUARDIAN"</span>
|
||||
<div class="information box defensive faint">
|
||||
"LAST TIME YOU PROTECTED" <Icon source=IconSource::ShieldAndSword />
|
||||
<div class="info-player-list">
|
||||
<IdentityInline ident=previous.into_public() />
|
||||
</div>
|
||||
<span class="yellow">
|
||||
"IF ATTACKED WHILE GUARDED, YOU AND THEIR ATTACKER WILL INSTEAD DIE"
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn GuardianPagePreviousGuard(previous: CharacterIdentity) -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="defensive box title">"GUARDIAN"</span>
|
||||
<div class="information box defensive faint">
|
||||
"LAST TIME YOU GUARDED" <Icon source=IconSource::ShieldAndSword />
|
||||
<div class="info-player-list">
|
||||
<IdentityInline ident=previous.into_public() />
|
||||
</div> <span class="yellow">"YOU CANNOT PROTECT THEM TONIGHT"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
|
||||
use crate::app::components::{Icon, IconSource};
|
||||
|
||||
#[component]
|
||||
pub fn HunterPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="offensive box title">HUNTER</span>
|
||||
<div class="information box offensive faint">
|
||||
"SET A HUNTER'S TRAP ON A PLAYER" <Icon source=IconSource::Hunter />
|
||||
<span class="yellow">
|
||||
IF YOU DIE TONIGHT, OR ARE EXECUTED TOMORROW
|
||||
THIS PLAYER WILL DIE AT NIGHT
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
use werewolves_proto::message::night::Visits;
|
||||
|
||||
use crate::app::components::{Icon, IconSource};
|
||||
|
||||
#[component]
|
||||
pub fn InsomniacPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"INSOMNIAC"</span>
|
||||
<div class="information box intel faint">
|
||||
"YOUR SLEEP IS INTERRUPTED" <Icon source=IconSource::Insomniac />
|
||||
"YOU'VE NOTICED VISITORS IN THE NIGHT"
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn InsomniacResult(visits: Visits) -> impl IntoView {
|
||||
let visitors = visits
|
||||
.iter()
|
||||
.map(|visitor| {
|
||||
view! {
|
||||
<div class="identity intel">
|
||||
<span class="number">{visitor.number.get()}</span>
|
||||
<span class="name">{visitor.name.clone()}</span>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
.collect_view();
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"INSOMNIAC"</span>
|
||||
<div class="information box intel faint">
|
||||
"YOU WERE VISITED IN THE NIGHT BY:" <div class="info-player-boxes">{visitors}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn LoneWolfPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="wolves box title">LONE WOLF</span>
|
||||
<div class="information box wolves faint">
|
||||
<span>
|
||||
YOU MUST KILL TONIGHT IN ANGER OVER A FELLOW
|
||||
WOLF HAVING BEEN SLAIN
|
||||
</span>
|
||||
<span class="yellow">PICK A PLAYER</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
|
||||
use crate::app::components::{Icon, IconSource};
|
||||
|
||||
#[component]
|
||||
pub fn MapleWolfPage1(nights_til_starvation: u8) -> impl IntoView {
|
||||
let food_state = if nights_til_starvation == 0 {
|
||||
view! {
|
||||
<span class="red">"YOU ARE STARVING"</span>
|
||||
"IF YOU FAIL TO EAT TONIGHT, YOU WILL DIE"
|
||||
}
|
||||
.into_any()
|
||||
} else {
|
||||
let nights = if nights_til_starvation == 1 {
|
||||
view! { <span class="red">"TOMORROW NIGHT "</span> }
|
||||
.into_any()
|
||||
} else {
|
||||
view! {
|
||||
"IN "
|
||||
<span class="red">{nights_til_starvation}</span>
|
||||
" NIGHTS "
|
||||
}
|
||||
.into_any()
|
||||
};
|
||||
view! {
|
||||
<span>
|
||||
"IF YOU FAIL TO EAT " {nights} "YOU WILL "<span class="yellow">"STARVE"</span>
|
||||
</span>
|
||||
}
|
||||
.into_any()
|
||||
};
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="offensive box title">"MAPLE WOLF"</span>
|
||||
<div class="information box offensive faint">
|
||||
"YOU CAN CHOOSE TO EAT A PLAYER TONIGHT" <Icon source=IconSource::MapleWolf />
|
||||
{food_state}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright (C) 2025-2026 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;
|
||||
|
||||
use leptos::prelude::*;
|
||||
use werewolves_proto::message::CharacterIdentity;
|
||||
|
||||
use crate::app::components::IdentityInline;
|
||||
|
||||
#[component]
|
||||
pub fn MasonRecruitPage1(recruits_left: NonZeroU8) -> impl IntoView {
|
||||
let recruitments = match recruits_left.get() {
|
||||
0 => unreachable!(),
|
||||
1 => view! {
|
||||
<span class="yellow">{1}</span>
|
||||
" RECRUITMENT"
|
||||
},
|
||||
num => view! {
|
||||
<span class="yellow">{num}</span>
|
||||
" RECRUITMENTS"
|
||||
},
|
||||
};
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"MASON LEADER"</span>
|
||||
<div class="information box intel faint">
|
||||
<span>"YOU HAVE "{recruitments}" LEFT"</span>
|
||||
<span>
|
||||
"RECRUITS WILL WAKE WITH YOU EVERY NIGHT"
|
||||
" WHILE THEY ARE ALIVE AND REMAIN VILLAGE ALIGNED"
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn MasonRecruitPage2() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"MASON LEADER"</span>
|
||||
<div class="information box intel faint">
|
||||
<span class="yellow">"WOULD YOU LIKE TO RECRUIT TONIGHT?"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn MasonsWake(leader: CharacterIdentity, masons: Box<[CharacterIdentity]>) -> impl IntoView {
|
||||
let title = view! {
|
||||
"MASONS OF "
|
||||
<span class="yellow">{leader.name.clone()}</span>
|
||||
};
|
||||
let masons = masons
|
||||
.iter()
|
||||
.map(|mason| {
|
||||
view! { <IdentityInline ident=mason.clone().into_public() /> }
|
||||
})
|
||||
.collect_view();
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">{title}</span>
|
||||
<div class="information box intel faint">
|
||||
"THE MASONS CONVENE AT NIGHT" <div class="info-player-list masons">{masons}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
|
||||
use crate::app::components::{Icon, IconSource};
|
||||
|
||||
#[component]
|
||||
pub fn MilitiaPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="offensive box title">"MILITIA"</span>
|
||||
<div class="information box offensive faint">
|
||||
<span>
|
||||
"IF YOU WISH TO USE YOUR " <span class="yellow">"ONCE PER GAME"</span>
|
||||
" KILL ABILITY"
|
||||
</span>
|
||||
<Icon source=IconSource::Sword />
|
||||
<span class="yellow">"PICK A PLAYER " "OR GO BACK TO SLEEP"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
use werewolves_proto::{diedto::DiedToTitle, message::PublicIdentity};
|
||||
|
||||
use crate::app::components::{Icon, IconSource, IdentityInline, PartialAssociatedIcon};
|
||||
|
||||
#[component]
|
||||
pub fn MorticianPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"MORTICIAN"</span>
|
||||
<div class="information box intel faint">
|
||||
<span>"PICK A "<span class="yellow">"DEAD"</span>" PLAYER"</span>
|
||||
<Icon source=IconSource::Mortician />
|
||||
<span class="yellow">"YOU WILL LEARN THE CAUSE " "OF THEIR DEATH"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn MorticianResultPage(died_to: DiedToTitle, target: PublicIdentity) -> impl IntoView {
|
||||
let text = match died_to {
|
||||
DiedToTitle::Execution => "Execution",
|
||||
DiedToTitle::MapleWolf => "Maple Wolf",
|
||||
DiedToTitle::MapleWolfStarved => "Starvation",
|
||||
DiedToTitle::Militia => "Militia Shot",
|
||||
DiedToTitle::Wolfpack => "Wolfpack",
|
||||
DiedToTitle::AlphaWolf => "Alpha Wolf",
|
||||
DiedToTitle::Shapeshift => "Shapeshifting",
|
||||
DiedToTitle::Hunter => "Hunter Trap",
|
||||
DiedToTitle::GuardianProtecting => "Guardian",
|
||||
DiedToTitle::PyreMaster => "Pyre Master",
|
||||
DiedToTitle::PyreMasterLynchMob => "An Angry Mob of Villagers Against Fire",
|
||||
DiedToTitle::MasonLeaderRecruitFail => "Occupational Hazard (Mason Recruit Fail)",
|
||||
DiedToTitle::LoneWolf => "Lone Wolf",
|
||||
};
|
||||
let icon = died_to.icon().unwrap_or(IconSource::Mortician);
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"MORTICIAN"</span>
|
||||
<div class="information box intel faint">
|
||||
<IdentityInline ident=target />
|
||||
"DIED TO"
|
||||
<Icon source=icon />
|
||||
<span class="yellow">{text}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
use werewolves_proto::{message::PublicIdentity, role::Powerful};
|
||||
|
||||
use crate::app::components::{Icon, IconSource, IdentityInline};
|
||||
|
||||
#[component]
|
||||
pub fn PowerSeerPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"POWER SEER"</span>
|
||||
<div class="information box intel faint">
|
||||
"PICK A PLAYER" <Icon source=IconSource::Powerful />
|
||||
<span class="yellow">"YOU WILL CHECK IF THEY ARE POWERFUL"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn PowerSeerResult(powerful: Powerful, target: PublicIdentity) -> impl IntoView {
|
||||
let text = match powerful {
|
||||
Powerful::Powerful => "IS POWERFUL",
|
||||
Powerful::NotPowerful => "IS NOT POWERFUL",
|
||||
};
|
||||
let icon = match powerful {
|
||||
Powerful::Powerful => IconSource::Powerful,
|
||||
Powerful::NotPowerful => IconSource::RedX,
|
||||
};
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"POWER SEER"</span>
|
||||
<div class="information box intel faint">
|
||||
<IdentityInline ident=target />
|
||||
<Icon source=icon />
|
||||
<span class="yellow">{text}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
|
||||
use crate::app::components::{Icon, IconSource};
|
||||
|
||||
#[component]
|
||||
pub fn ProtectorPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="defensive box title">"PROTECTOR"</span>
|
||||
<div class="information box defensive faint">
|
||||
"PICK A PLAYER" <Icon source=IconSource::Shield />
|
||||
<span class="yellow">"YOU WILL PROTECT THEM FROM A DEATH TONIGHT"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
|
||||
use crate::app::components::{Icon, IconSource};
|
||||
|
||||
#[component]
|
||||
pub fn PyremasterPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="offensive box title">"PYREMASTER"</span>
|
||||
<div class="information box offensive faint">
|
||||
"YOU CAN CHOOSE TO THROW A PLAYER ON THE PYRE"
|
||||
<Icon source=IconSource::Pyremaster />
|
||||
<span>
|
||||
"IF YOU KILL " <span class="yellow">"TWO"</span> " GOOD VILLAGERS LIKE THIS "
|
||||
"YOU WILL DIE AS WELL"
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
use werewolves_proto::{
|
||||
message::PublicIdentity,
|
||||
role::{Alignment, RoleTitle},
|
||||
};
|
||||
|
||||
use crate::app::components::{AssociatedIcon, Icon, IconSource, IdentityInline, RoleTitleSpan};
|
||||
|
||||
#[component]
|
||||
pub fn SeerPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"SEER"</span>
|
||||
<div class="information box intel faint">
|
||||
"PICK A PLAYER" <div class="seer-icons">
|
||||
<Icon source=IconSource::Village />
|
||||
<Icon source=IconSource::Wolves />
|
||||
</div> <span class="yellow">"YOU WILL CHECK THEIR ALIGNMENT"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn SeerResult(alignment: Alignment, target: PublicIdentity) -> impl IntoView {
|
||||
let text = match alignment {
|
||||
Alignment::Village => "VILLAGE",
|
||||
Alignment::Wolves => "WOLFPACK",
|
||||
Alignment::Damned => "DAMNED",
|
||||
};
|
||||
let additional_info = match alignment {
|
||||
Alignment::Village => view! { <FalselyAppearsAs roles=RoleTitle::falsely_appear_village() alignment_text=text /> }
|
||||
.into_any(),
|
||||
Alignment::Wolves => view! { <FalselyAppearsAs roles=RoleTitle::falsely_appear_wolf() alignment_text=text /> }
|
||||
.into_any(),
|
||||
Alignment::Damned => view! {
|
||||
<div class="bottom-bound">
|
||||
"THIS PERSON IS " <span class="yellow">"DAMNED"</span> "THEY WIN ALONGSIDE EVIL"
|
||||
</div>
|
||||
}
|
||||
.into_any(),
|
||||
};
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="intel box title">"SEER"</span>
|
||||
<div class="information box intel faint">
|
||||
<div class="two-column">
|
||||
<div class="seer-check">
|
||||
<IdentityInline ident=target />
|
||||
<Icon source=alignment.icon() />
|
||||
<span class="yellow">{text}</span>
|
||||
</div>
|
||||
{additional_info}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn FalselyAppearsAs(
|
||||
#[allow(clippy::boxed_local)] roles: Box<[RoleTitle]>,
|
||||
alignment_text: &'static str,
|
||||
) -> impl IntoView {
|
||||
let false_positives = roles
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|role| {
|
||||
view! { <RoleTitleSpan role=role /> }
|
||||
})
|
||||
.collect_view();
|
||||
view! {
|
||||
<div class="bottom-bound">
|
||||
"ROLES THAT FALSELY APPEAR AS " <span class="yellow">{alignment_text}</span>
|
||||
<div class="false-positives yellow">{false_positives}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
|
||||
use crate::app::components::{Icon, IconSource};
|
||||
|
||||
#[component]
|
||||
pub fn VindicatorPage1() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="defensive box title">"VINDICATOR"</span>
|
||||
<div class="information box defensive faint">
|
||||
<span>"A VILLAGER WAS EXECUTED"</span>
|
||||
<Icon source=IconSource::Vindicator />
|
||||
<span class="yellow">"PICK A PLAYER TO PROTECT FROM A DEATH TONIGHT"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
use werewolves_proto::role::RoleTitle;
|
||||
|
||||
use crate::app::{
|
||||
class::{AsClasses, Class},
|
||||
components::{Icon, PartialAssociatedIcon},
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn RoleChange(role: RoleTitle) -> impl IntoView {
|
||||
let class = role.category().class();
|
||||
let icon = role.icon().map(|icon| {
|
||||
view! { <Icon source=icon /> }
|
||||
});
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class=[class, "box", "title"].as_classes()>{"ROLE CHANGE"}</span>
|
||||
<div class=["information", "box", class, "faint"]
|
||||
.as_classes()>
|
||||
{"YOUR ROLE HAS CHANGED"} {icon}
|
||||
<span>
|
||||
{"YOUR NEW ROLE IS "}
|
||||
<span class="yellow">{role.to_string().to_case(Case::Upper)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
|
||||
use crate::app::components::{Icon, IconSource};
|
||||
|
||||
#[component]
|
||||
pub fn RoleblockPage() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="wolves box title">"ROLE BLOCKED"</span>
|
||||
<div class="information box wolves faint">
|
||||
"YOU WERE ROLE BLOCKED" <Icon source=IconSource::Roleblock />
|
||||
<span class="yellow">"YOUR NIGHT ACTION DID NOT TAKE PLACE"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
|
||||
use crate::app::components::{Icon, IconSource};
|
||||
|
||||
#[component]
|
||||
pub fn ShiftFailed() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="wolves box title">"SHIFT FAILED"</span>
|
||||
<div class="information box wolves faint">
|
||||
"YOUR SHIFT HAS FAILED" <Icon source=IconSource::RedX />
|
||||
"YOU RETAIN YOUR SHAPESHIFT ABILITY"
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright (C) 2025-2026 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 leptos::prelude::*;
|
||||
use werewolves_proto::{message::CharacterIdentity, role::RoleTitle};
|
||||
|
||||
use crate::app::components::{Icon, IconSource, IdentityInline};
|
||||
|
||||
#[component]
|
||||
pub fn WolvesIntro(wolves: Box<[(CharacterIdentity, RoleTitle)]>) -> impl IntoView {
|
||||
let wolves = wolves
|
||||
.into_iter()
|
||||
.map(|w| {
|
||||
view! {
|
||||
<div class="character wolves box faint">
|
||||
<span class="role yellow">{w.1.to_string().to_case(Case::Title)}</span>
|
||||
<IdentityInline ident=w.0.into_public() />
|
||||
</div>
|
||||
}
|
||||
})
|
||||
.collect_view();
|
||||
view! {
|
||||
<div class="role-page wolves-intro">
|
||||
<span class="wolves box title">"THESE ARE THE WOLVES"</span>
|
||||
<div class="wolves-list">{wolves}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn WolfPackKill() -> impl IntoView {
|
||||
view! {
|
||||
<div class="role-page">
|
||||
<span class="wolves box title">"WOLF PACK KILL"</span>
|
||||
<div class="information box wolves faint">
|
||||
"CHOOSE A TARGET TO EAT TONIGHT" <Icon source=IconSource::Wolves />
|
||||
<span class="yellow">"WOLVES MUST BE UNANIMOUS"</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ pub fn NotFound() -> impl IntoView {
|
|||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
<h2>{"not found"}</h2>
|
||||
<h2>not found</h2>
|
||||
<h4>"specifically, this is the 404 page"</h4>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ impl<'a> GameRunner<'a> {
|
|||
self.game.game_over()
|
||||
}
|
||||
|
||||
pub fn get_game(&self) -> &Game {
|
||||
&self.game
|
||||
}
|
||||
|
||||
pub async fn process(&mut self, msg: HostOrClientMessage) -> Option<GameOver> {
|
||||
let start = Utc::now();
|
||||
let msg = match msg {
|
||||
|
|
|
|||
|
|
@ -94,11 +94,24 @@ impl Host {
|
|||
}
|
||||
|
||||
async fn send_message(&mut self, msg: ServerToHostMessage) -> Result<(), anyhow::Error> {
|
||||
log::debug!(
|
||||
match &msg {
|
||||
ServerToHostMessage::ActionPrompt(prompt, _) => log::debug!(
|
||||
"sending ActionPrompt({}) message to {}",
|
||||
prompt.title().to_string().bold(),
|
||||
self.who.dimmed()
|
||||
),
|
||||
ServerToHostMessage::ActionResult(_, result) => log::debug!(
|
||||
"sending ActionResult({}) message to {}",
|
||||
result.title().to_string().bold(),
|
||||
self.who.dimmed()
|
||||
),
|
||||
_ => log::debug!(
|
||||
"sending {} message to {}",
|
||||
msg.title().to_string().bold(),
|
||||
self.who.dimmed()
|
||||
);
|
||||
),
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.socket
|
||||
.send(ws::Message::Binary(
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@ pub async fn run_game(
|
|||
.await
|
||||
.is_some()
|
||||
{
|
||||
log::info!("game over, man!");
|
||||
game.game_state = GameRecordState::GameOver(current_game.story());
|
||||
if let Err(err) = db.game().store_game_state(&game).await {
|
||||
log::error!("saving game({game_id}) state: {err}; quitting...");
|
||||
|
|
@ -207,6 +208,12 @@ pub async fn run_game(
|
|||
}
|
||||
break;
|
||||
}
|
||||
game.game_state = GameRecordState::Started(runner.get_game().clone());
|
||||
log::info!("saving game state");
|
||||
if let Err(err) = db.game().store_game_state(&game).await {
|
||||
log::error!("saving game({game_id}) state: {err}; quitting...");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
GameRecordState::GameOver(story) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue