daytime port
This commit is contained in:
parent
80859f58d0
commit
c8e51f36e2
|
|
@ -1,5 +1,5 @@
|
||||||
.icon-fit {
|
.icon-fit {
|
||||||
// height: 1em;
|
height: 1em;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -935,3 +935,96 @@ form {
|
||||||
gap: 1ch;
|
gap: 1ch;
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.top-of-day-info {
|
||||||
|
width: max-content;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
gap: 10vw;
|
||||||
|
|
||||||
|
.info-tidbit {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 1.5em;
|
||||||
|
opacity: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parity {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parity-pct {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-nights-kills {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2cm;
|
||||||
|
|
||||||
|
.identity {
|
||||||
|
.number {
|
||||||
|
color: red;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-day {
|
||||||
|
color: red;
|
||||||
|
font-size: 3em;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5ch;
|
||||||
|
padding-bottom: 1ch;
|
||||||
|
|
||||||
|
.character {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1ch;
|
||||||
|
padding: 1ch;
|
||||||
|
font-size: 1.25em;
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 0.5ch;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
background-color: $village_color_faint;
|
||||||
|
border: 1px solid $village_border_faint;
|
||||||
|
|
||||||
|
&.marked {
|
||||||
|
background-color: color.change($red2, $alpha: 0.3);
|
||||||
|
border: 1px solid color.change($red2, $alpha: 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
.cover-of-darkness {
|
.cover-of-darkness {
|
||||||
|
background-color: black;
|
||||||
font-size: 3em;
|
font-size: 3em;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
|
||||||
|
|
@ -164,12 +164,10 @@ pub fn BigScreen() -> impl IntoView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ServerToHostMessage::Disconnect => disconnect.set(true),
|
ServerToHostMessage::Disconnect => disconnect.set(true),
|
||||||
ServerToHostMessage::Daytime {
|
ServerToHostMessage::Daytime { settings: s, .. } => {
|
||||||
characters,
|
settings.set(s);
|
||||||
marked,
|
page.set(BigScreenPage::Setup);
|
||||||
day,
|
}
|
||||||
settings,
|
|
||||||
} => todo!(),
|
|
||||||
ServerToHostMessage::PlayerStates(_) => {}
|
ServerToHostMessage::PlayerStates(_) => {}
|
||||||
ServerToHostMessage::ActionPrompt(act, ppage) => {
|
ServerToHostMessage::ActionPrompt(act, ppage) => {
|
||||||
page.set(BigScreenPage::ActionPrompt {
|
page.set(BigScreenPage::ActionPrompt {
|
||||||
|
|
@ -229,10 +227,15 @@ pub fn BigScreen() -> impl IntoView {
|
||||||
BigScreenPage::ActionPrompt { prompt, page } => {
|
BigScreenPage::ActionPrompt { prompt, page } => {
|
||||||
view! { <RolePrompt prompt=prompt page=page /> }.into_any()
|
view! { <RolePrompt prompt=prompt page=page /> }.into_any()
|
||||||
}
|
}
|
||||||
BigScreenPage::ActionResult { character, result } => view! { <RoleResult character=character result=result /> }
|
BigScreenPage::ActionResult { character, result } => {
|
||||||
.into_any(),
|
view! { <RoleResult character=character result=result /> }.into_any()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
view! { <div class="big-screen-wrapper">{content}</div> }.into_any()
|
view! {
|
||||||
|
<div class:big-screen-wrapper=move || {
|
||||||
|
!matches!(page.get(), BigScreenPage::Setup)
|
||||||
|
}>{content}</div>
|
||||||
|
}.into_any()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
werewolves_macros::include_path!("werewolves/src/app/pages/game/host");
|
werewolves_macros::include_path!("werewolves/src/app/pages/game/host");
|
||||||
|
|
||||||
|
use core::num::NonZeroU8;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
|
character::CharacterId,
|
||||||
game::{Category, GameSettings},
|
game::{Category, GameSettings},
|
||||||
message::{
|
message::{
|
||||||
CharacterIdentity, PlayerState,
|
CharacterIdentity, CharacterState, PlayerState,
|
||||||
host::{
|
host::{HostGameMessage, HostLobbyMessage, HostMessage, ServerToHostMessage as Srv2Host},
|
||||||
HostGameMessage, HostLobbyMessage, HostMessage, HostNightMessage,
|
|
||||||
ServerToHostMessage as Srv2Host,
|
|
||||||
},
|
|
||||||
night::{ActionPrompt, ActionResult},
|
night::{ActionPrompt, ActionResult},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -36,6 +35,12 @@ enum HostPage {
|
||||||
character: Option<CharacterIdentity>,
|
character: Option<CharacterIdentity>,
|
||||||
result: ActionResult,
|
result: ActionResult,
|
||||||
},
|
},
|
||||||
|
Daytime {
|
||||||
|
day: NonZeroU8,
|
||||||
|
characters: Box<[CharacterState]>,
|
||||||
|
marked: Box<[CharacterId]>,
|
||||||
|
reply: WriteSignal<Option<HostMessage>>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
|
@ -118,6 +123,19 @@ pub fn HostGamePage(
|
||||||
Srv2Host::ActionResult(character, result) => {
|
Srv2Host::ActionResult(character, result) => {
|
||||||
page.set(HostPage::ActionResult { character, result })
|
page.set(HostPage::ActionResult { character, result })
|
||||||
}
|
}
|
||||||
|
Srv2Host::Daytime {
|
||||||
|
characters,
|
||||||
|
marked,
|
||||||
|
day,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
page.set(HostPage::Daytime {
|
||||||
|
day,
|
||||||
|
characters,
|
||||||
|
marked,
|
||||||
|
reply,
|
||||||
|
});
|
||||||
|
}
|
||||||
_ => log::error!("{message:#?}"),
|
_ => log::error!("{message:#?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -165,6 +183,15 @@ pub fn HostGamePage(
|
||||||
view! { <RoleResult reply=reply_result.write_only() character=character result=result /> }
|
view! { <RoleResult reply=reply_result.write_only() character=character result=result /> }
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
|
HostPage::Daytime {
|
||||||
|
day,
|
||||||
|
characters,
|
||||||
|
marked,
|
||||||
|
reply,
|
||||||
|
} => view! {
|
||||||
|
<DaytimePlayerList day=day characters=characters marked=marked reply=reply/>
|
||||||
|
}
|
||||||
|
.into_any(),
|
||||||
};
|
};
|
||||||
view! {
|
view! {
|
||||||
{cancel}
|
{cancel}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
// 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, ops::Not};
|
||||||
|
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos::{IntoView, prelude::WriteSignal};
|
||||||
|
use werewolves_proto::message::host::{HostDayMessage, HostGameMessage};
|
||||||
|
use werewolves_proto::{
|
||||||
|
character::CharacterId,
|
||||||
|
game::GameTime,
|
||||||
|
message::{CharacterState, host::HostMessage},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::app::components::{
|
||||||
|
AssociatedIcon, DialogModal, Icon, IconType, IdentityInline, PartialAssociatedIcon,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn DaytimePlayerList(
|
||||||
|
#[prop(optional)] day: Option<NonZeroU8>,
|
||||||
|
characters: Box<[CharacterState]>,
|
||||||
|
marked: Box<[CharacterId]>,
|
||||||
|
reply: WriteSignal<Option<HostMessage>>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let mut characters = characters.clone();
|
||||||
|
let last_nights_kills = {
|
||||||
|
let kills = characters
|
||||||
|
.iter()
|
||||||
|
.filter_map(|c| {
|
||||||
|
c.died_to
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|died_to| match died_to.date_time() {
|
||||||
|
GameTime::Day { .. } => None,
|
||||||
|
GameTime::Night { number } => {
|
||||||
|
if let Some(day) = day.as_ref()
|
||||||
|
&& number == day.get() - 1
|
||||||
|
{
|
||||||
|
Some(c)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(|killed| {
|
||||||
|
view! {
|
||||||
|
<span>
|
||||||
|
<IdentityInline ident=killed.identity.clone().into_public() />
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
kills.is_empty().not().then_some(view! {
|
||||||
|
<div class="info-tidbit">
|
||||||
|
<label>{"died last night"}</label>
|
||||||
|
<div class="last-nights-kills">{kills.into_iter().collect_view()}</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
};
|
||||||
|
characters.sort_by(|l, r| l.identity.number.cmp(&r.identity.number));
|
||||||
|
let chars = characters
|
||||||
|
.iter()
|
||||||
|
.map(|c| {
|
||||||
|
view! {
|
||||||
|
<DaytimePlayer
|
||||||
|
character=c.clone()
|
||||||
|
reply=reply
|
||||||
|
marked=marked.contains(&c.identity.character_id)
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect_view();
|
||||||
|
let (button_text, confirmation_text) = if marked.is_empty() {
|
||||||
|
(
|
||||||
|
"end day".to_string(),
|
||||||
|
"really end the day with no executions?".to_string(),
|
||||||
|
)
|
||||||
|
} else if marked.len() == 1 {
|
||||||
|
(
|
||||||
|
"execute 1 player".to_string(),
|
||||||
|
characters
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.identity.character_id == marked[0])
|
||||||
|
.map(|c| c.identity.clone().into_public())
|
||||||
|
.map(|id| format!("really execute {id}?"))
|
||||||
|
.unwrap_or("really execute 1 player?".to_string()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
format!("execute {} players", marked.len()),
|
||||||
|
format!("really execute {} players?", marked.len()),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let parity = {
|
||||||
|
let wolves = characters
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.died_to.is_none() && c.role.wolf())
|
||||||
|
.count();
|
||||||
|
let total = characters.iter().filter(|c| c.died_to.is_none()).count();
|
||||||
|
let pct_parity = (((wolves as f64) * 100.0) / (total as f64)).round();
|
||||||
|
view! {
|
||||||
|
<div class="info-tidbit">
|
||||||
|
<label>{"parity"}</label>
|
||||||
|
<span class="parity">
|
||||||
|
<span class="red">{wolves}</span>
|
||||||
|
{"/"}
|
||||||
|
<span class="total">{total}</span>
|
||||||
|
</span>
|
||||||
|
<span class="parity-pct">{"("} {pct_parity} {"%)"}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let button = view! {
|
||||||
|
<DialogModal button_content=button_text.clone()>
|
||||||
|
<h3>{confirmation_text.clone()}</h3>
|
||||||
|
<button on:click=move |_| {
|
||||||
|
reply
|
||||||
|
.set(
|
||||||
|
Some(HostMessage::InGame(HostGameMessage::Day(HostDayMessage::Execute))),
|
||||||
|
)
|
||||||
|
}>{button_text.clone()}</button>
|
||||||
|
</DialogModal>
|
||||||
|
};
|
||||||
|
let day = day.as_ref().map(|day| {
|
||||||
|
view! {
|
||||||
|
<div class="info-tidbit">
|
||||||
|
<label>{"day"}</label>
|
||||||
|
<span class="current-day">{day.get()}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
view! {
|
||||||
|
<div class="character-picker">
|
||||||
|
<div class="top-of-day-info">{day} {parity} {last_nights_kills}</div>
|
||||||
|
<div class="player-list">{chars}</div>
|
||||||
|
{button}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn DaytimePlayer(
|
||||||
|
character: CharacterState,
|
||||||
|
reply: WriteSignal<Option<HostMessage>>,
|
||||||
|
marked: bool,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let CharacterState {
|
||||||
|
identity,
|
||||||
|
role,
|
||||||
|
died_to,
|
||||||
|
..
|
||||||
|
} = character;
|
||||||
|
let character_id = identity.character_id;
|
||||||
|
let select = move |_| {
|
||||||
|
reply.set(Some(HostMessage::InGame(HostGameMessage::Day(
|
||||||
|
HostDayMessage::MarkForExecution(character_id),
|
||||||
|
))))
|
||||||
|
};
|
||||||
|
|
||||||
|
let icon = role.icon().unwrap_or_else(|| role.alignment().icon());
|
||||||
|
let text = role.to_string().to_case(Case::Title);
|
||||||
|
let align_class = role.wolf().then_some("red");
|
||||||
|
view! {
|
||||||
|
<button on:click=select class="character no-hover" class:dead=died_to.is_some() class:marked=marked>
|
||||||
|
<div class="day-char">
|
||||||
|
<span class="headline">
|
||||||
|
<Icon source=icon r#type=IconType::Small />
|
||||||
|
<span class=align_class>{text}</span>
|
||||||
|
</span>
|
||||||
|
<IdentityInline ident=identity.into_public() />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,21 +23,39 @@ pub fn RoleResult(
|
||||||
#[prop(optional)] reply: Option<WriteSignal<Option<HostNightMessage>>>,
|
#[prop(optional)] reply: Option<WriteSignal<Option<HostNightMessage>>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let body = match result {
|
let body = match result {
|
||||||
ActionResult::RoleBlocked => view! { <RoleblockPage /> }.into_any(),
|
ActionResult::RoleBlocked => view!{
|
||||||
ActionResult::Drunk => view! { <DrunkPage /> }.into_any(),
|
<RoleblockPage />
|
||||||
ActionResult::Seer(target, alignment) => view! { <SeerResult target=target.into_public() alignment=alignment /> }.into_any(),
|
}.into_any(),
|
||||||
ActionResult::PowerSeer { target, powerful } => view! { <PowerSeerResult target=target.into_public() powerful=powerful /> }.into_any(),
|
ActionResult::Drunk => view! {
|
||||||
ActionResult::Adjudicator { target, killer } => view! { <AdjudicatorResult target=target.into_public() killer=killer /> }.into_any(),
|
<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! {
|
ActionResult::Arcanist((target1, target2), alignment_eq) => view! {
|
||||||
<ArcanistResult
|
<ArcanistResult
|
||||||
value=alignment_eq
|
value=alignment_eq
|
||||||
targets=(target1.into_public(), target2.into_public())
|
targets=(target1.into_public(), target2.into_public())
|
||||||
/>
|
/>
|
||||||
}.into_any(),
|
}.into_any(),
|
||||||
ActionResult::GraveDigger(target, role) => view! { <GravediggerResultPage target=target.into_public() role=role /> }.into_any(),
|
ActionResult::GraveDigger(target, role) => view! {
|
||||||
ActionResult::Mortician(target, died_to) => view! { <MorticianResultPage target=target.into_public() died_to=died_to /> }.into_any(),
|
<GravediggerResultPage target=target.into_public() role=role/>
|
||||||
ActionResult::Insomniac(visits) => view! { <InsomniacResult visits=visits /> }.into_any(),
|
}.into_any(),
|
||||||
ActionResult::Empath { target, scapegoat } => view! { <EmpathResult target=target.into_public() scapegoat=scapegoat /> }.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::BeholderSawNothing => todo!(),
|
||||||
ActionResult::BeholderSawEverything => todo!(),
|
ActionResult::BeholderSawEverything => todo!(),
|
||||||
ActionResult::GoBackToSleep => return match reply {
|
ActionResult::GoBackToSleep => return match reply {
|
||||||
|
|
@ -51,9 +69,7 @@ pub fn RoleResult(
|
||||||
let Some(reply) = reply else {
|
let Some(reply) = reply else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
reply.set(Some(HostNightMessage::ActionResponse(
|
reply.set(Some(HostNightMessage::Next));
|
||||||
ActionResponse::ContinueToResult,
|
|
||||||
)));
|
|
||||||
});
|
});
|
||||||
().into_any()
|
().into_any()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#![allow(clippy::expect_fun_call)]
|
#![allow(clippy::expect_fun_call)]
|
||||||
|
#![allow(clippy::boxed_local)]
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
mod ssr {
|
mod ssr {
|
||||||
pub const DEFAULT_MAX_PG_CONNECTIONS: u32 = 30;
|
pub const DEFAULT_MAX_PG_CONNECTIONS: u32 = 30;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue