daytime port
This commit is contained in:
parent
80859f58d0
commit
c8e51f36e2
|
|
@ -1,5 +1,5 @@
|
|||
.icon-fit {
|
||||
// height: 1em;
|
||||
height: 1em;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
|
|
|
|||
|
|
@ -935,3 +935,96 @@ form {
|
|||
gap: 1ch;
|
||||
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 {
|
||||
background-color: black;
|
||||
font-size: 3em;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
|
|||
|
|
@ -164,12 +164,10 @@ pub fn BigScreen() -> impl IntoView {
|
|||
}
|
||||
}
|
||||
ServerToHostMessage::Disconnect => disconnect.set(true),
|
||||
ServerToHostMessage::Daytime {
|
||||
characters,
|
||||
marked,
|
||||
day,
|
||||
settings,
|
||||
} => todo!(),
|
||||
ServerToHostMessage::Daytime { settings: s, .. } => {
|
||||
settings.set(s);
|
||||
page.set(BigScreenPage::Setup);
|
||||
}
|
||||
ServerToHostMessage::PlayerStates(_) => {}
|
||||
ServerToHostMessage::ActionPrompt(act, ppage) => {
|
||||
page.set(BigScreenPage::ActionPrompt {
|
||||
|
|
@ -229,10 +227,15 @@ pub fn BigScreen() -> impl IntoView {
|
|||
BigScreenPage::ActionPrompt { prompt, page } => {
|
||||
view! { <RolePrompt prompt=prompt page=page /> }.into_any()
|
||||
}
|
||||
BigScreenPage::ActionResult { character, result } => view! { <RoleResult character=character result=result /> }
|
||||
.into_any(),
|
||||
BigScreenPage::ActionResult { character, result } => {
|
||||
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");
|
||||
|
||||
use core::num::NonZeroU8;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use leptos::prelude::*;
|
||||
use werewolves_proto::{
|
||||
character::CharacterId,
|
||||
game::{Category, GameSettings},
|
||||
message::{
|
||||
CharacterIdentity, PlayerState,
|
||||
host::{
|
||||
HostGameMessage, HostLobbyMessage, HostMessage, HostNightMessage,
|
||||
ServerToHostMessage as Srv2Host,
|
||||
},
|
||||
CharacterIdentity, CharacterState, PlayerState,
|
||||
host::{HostGameMessage, HostLobbyMessage, HostMessage, ServerToHostMessage as Srv2Host},
|
||||
night::{ActionPrompt, ActionResult},
|
||||
},
|
||||
};
|
||||
|
|
@ -36,6 +35,12 @@ enum HostPage {
|
|||
character: Option<CharacterIdentity>,
|
||||
result: ActionResult,
|
||||
},
|
||||
Daytime {
|
||||
day: NonZeroU8,
|
||||
characters: Box<[CharacterState]>,
|
||||
marked: Box<[CharacterId]>,
|
||||
reply: WriteSignal<Option<HostMessage>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[component]
|
||||
|
|
@ -118,6 +123,19 @@ pub fn HostGamePage(
|
|||
Srv2Host::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:#?}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -165,6 +183,15 @@ pub fn HostGamePage(
|
|||
view! { <RoleResult reply=reply_result.write_only() character=character result=result /> }
|
||||
.into_any()
|
||||
}
|
||||
HostPage::Daytime {
|
||||
day,
|
||||
characters,
|
||||
marked,
|
||||
reply,
|
||||
} => view! {
|
||||
<DaytimePlayerList day=day characters=characters marked=marked reply=reply/>
|
||||
}
|
||||
.into_any(),
|
||||
};
|
||||
view! {
|
||||
{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>>>,
|
||||
) -> 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::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::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 {
|
||||
|
|
@ -51,9 +69,7 @@ pub fn RoleResult(
|
|||
let Some(reply) = reply else {
|
||||
return;
|
||||
};
|
||||
reply.set(Some(HostNightMessage::ActionResponse(
|
||||
ActionResponse::ContinueToResult,
|
||||
)));
|
||||
reply.set(Some(HostNightMessage::Next));
|
||||
});
|
||||
().into_any()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#![allow(clippy::expect_fun_call)]
|
||||
|
||||
#![allow(clippy::boxed_local)]
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr {
|
||||
pub const DEFAULT_MAX_PG_CONNECTIONS: u32 = 30;
|
||||
|
|
|
|||
Loading…
Reference in New Issue