role reveal for host
This commit is contained in:
parent
f280f35305
commit
d3d2819e12
|
|
@ -784,3 +784,80 @@ form {
|
|||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.start-game-status {
|
||||
min-height: 6ch;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:has(.error) {
|
||||
border: 1px solid rgb(128, 0, 0);
|
||||
background-color: rgba(255, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: 1.5em;
|
||||
|
||||
&::before {
|
||||
content: '⚠️';
|
||||
margin-right: 1ch;
|
||||
}
|
||||
}
|
||||
|
||||
.start-game {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.start-game-dialog-info {
|
||||
word-wrap: normal;
|
||||
|
||||
margin-bottom: 1ch;
|
||||
}
|
||||
|
||||
|
||||
.host-role-reveal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25ch;
|
||||
|
||||
.force-all {
|
||||
font-size: 1.25em;
|
||||
margin-top: 1ch;
|
||||
margin-bottom: 1ch;
|
||||
}
|
||||
}
|
||||
|
||||
.role-reveal-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25ch;
|
||||
|
||||
.player {
|
||||
@media only screen and (min-width : 1199px) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25ch;
|
||||
padding: 0.5ch;
|
||||
|
||||
.identity {
|
||||
align-self: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&.ready {
|
||||
background-color: $village_color_faint;
|
||||
border: 1px solid $village_color;
|
||||
|
||||
button {
|
||||
opacity: 0%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ pub enum GameError {
|
|||
HostChannelClosed,
|
||||
#[error("too many players: there's {got} players but only {need} roles")]
|
||||
TooManyPlayers { got: u8, need: u8 },
|
||||
#[error(
|
||||
"too few players: there's {got} players and this setup would require {need} roles to not be at instant parity"
|
||||
)]
|
||||
TooFewPlayers { got: u32, need: u32 },
|
||||
#[error("it's already daytime")]
|
||||
AlreadyDaytime,
|
||||
#[error("it's not the end of the night yet")]
|
||||
|
|
|
|||
|
|
@ -182,6 +182,12 @@ impl GameSettings {
|
|||
|
||||
pub fn check_with_player_list(&self, players: &[Identification]) -> Result<()> {
|
||||
self.check()?;
|
||||
if self.min_players_needed() > players.len() {
|
||||
return Err(GameError::TooFewPlayers {
|
||||
got: players.len() as _,
|
||||
need: self.min_players_needed() as _,
|
||||
});
|
||||
}
|
||||
let (p_len, r_len) = (players.len(), self.roles.len());
|
||||
if p_len > r_len {
|
||||
return Err(GameError::TooManyPlayers {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ pub enum HostMessage {
|
|||
Lobby(HostLobbyMessage),
|
||||
InGame(HostGameMessage),
|
||||
ForceRoleAckFor(CharacterId),
|
||||
#[cfg(debug_assertions)]
|
||||
ForceAllRoleAcks,
|
||||
PostGame(PostGameMessage),
|
||||
Echo(ServerToHostMessage),
|
||||
CancelGame,
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ pub fn ChangePlayerNumber(
|
|||
on:input:target=update
|
||||
value=move || { number.get().map(|n| n.get().to_string()).unwrap_or_default() }
|
||||
/>
|
||||
<input value="submit" type="submit" on:click=submit_click/>
|
||||
<input value="submit" type="submit" on:click=submit_click />
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ enum HostPage {
|
|||
#[default]
|
||||
None,
|
||||
Settings,
|
||||
RoleRevealAcks,
|
||||
}
|
||||
|
||||
#[component]
|
||||
|
|
@ -33,6 +34,8 @@ pub fn HostGamePage(
|
|||
let qr_mode = RwSignal::new(false);
|
||||
let players: RwSignal<Box<[PlayerState]>> = RwSignal::new(Box::new([]));
|
||||
let dialog_open = RwSignal::new(HashMap::new());
|
||||
let acks: RwSignal<Box<[RoleRevealCharacter]>> = RwSignal::new(Box::new([]));
|
||||
|
||||
let open_categories = RwSignal::new(
|
||||
Category::ALL
|
||||
.into_iter()
|
||||
|
|
@ -74,6 +77,22 @@ pub fn HostGamePage(
|
|||
Srv2Host::Error(err) => {
|
||||
error.set(Some(err.to_string()));
|
||||
}
|
||||
Srv2Host::WaitingForRoleRevealAcks { ackd, waiting } => {
|
||||
let mut reveals = ackd
|
||||
.into_iter()
|
||||
.map(|a| RoleRevealCharacter {
|
||||
char: a,
|
||||
acknowledged: true,
|
||||
})
|
||||
.chain(waiting.into_iter().map(|w| RoleRevealCharacter {
|
||||
char: w,
|
||||
acknowledged: false,
|
||||
}))
|
||||
.collect::<Box<_>>();
|
||||
reveals.sort_by_key(|r| r.char.number);
|
||||
acks.set(reveals);
|
||||
page.set(HostPage::RoleRevealAcks);
|
||||
}
|
||||
_ => log::error!("{message:#?}"),
|
||||
}
|
||||
}
|
||||
|
|
@ -98,6 +117,9 @@ pub fn HostGamePage(
|
|||
/>
|
||||
}
|
||||
.into_any(),
|
||||
HostPage::RoleRevealAcks => {
|
||||
view! { <RoleRevealAcks reply=reply error=error acks=acks.read_only() /> }.into_any()
|
||||
}
|
||||
};
|
||||
view! {
|
||||
{cancel}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ pub fn HostPlayerList(
|
|||
<IdentificationInline ident=sample_no_number />
|
||||
</button>
|
||||
<Equals />
|
||||
<span class="ok">"no number assigned"</span>
|
||||
<span class="ok">"no seat assigned"</span>
|
||||
</Sample>
|
||||
</TutorialBox>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
use core::ops::Not;
|
||||
|
||||
use leptos::{ev::MouseEvent, prelude::*};
|
||||
use werewolves_proto::message::{CharacterIdentity, host::HostMessage};
|
||||
|
||||
use crate::app::components::IdentityInline;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RoleRevealCharacter {
|
||||
pub char: CharacterIdentity,
|
||||
pub acknowledged: bool,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RoleRevealAcks(
|
||||
reply: WriteSignal<Option<HostMessage>>,
|
||||
acks: ReadSignal<Box<[RoleRevealCharacter]>>,
|
||||
error: WriteSignal<Option<String>>,
|
||||
) -> impl IntoView {
|
||||
let acks = move || {
|
||||
acks.get()
|
||||
.into_iter()
|
||||
.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 />
|
||||
<button disabled=acknowledged on:click=force_ready>
|
||||
"force ready"
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
};
|
||||
|
||||
let force_all = move || {
|
||||
(option_env!("LEPTOS_WATCH") == Some("true")).then_some({
|
||||
let force = move |ev: MouseEvent| {
|
||||
ev.prevent_default();
|
||||
#[cfg(debug_assertions)]
|
||||
reply.set(Some(HostMessage::ForceAllRoleAcks));
|
||||
};
|
||||
view! { <button on:click=force class="force-all">"force all"</button> }
|
||||
})
|
||||
};
|
||||
|
||||
move || {
|
||||
view! { <div class="host-role-reveal">{force_all} <div class="role-reveal-list">{acks}</div></div> }
|
||||
}
|
||||
}
|
||||
|
|
@ -37,13 +37,32 @@ pub fn Settings(
|
|||
.cloned()
|
||||
.map(move |s| {
|
||||
let signal = RwSignal::new(s);
|
||||
Effect::new(move || {
|
||||
let mut s = settings.get();
|
||||
s.update_slot(signal.get());
|
||||
reply.set(Some(HostMessage::Lobby(HostLobbyMessage::SetGameSettings(s))));
|
||||
});
|
||||
let remove = RwSignal::new(false);
|
||||
Effect::new(move || {
|
||||
let mut s = settings.get();
|
||||
s.update_slot(signal.get());
|
||||
reply.set(Some(HostMessage::Lobby(HostLobbyMessage::SetGameSettings(
|
||||
s,
|
||||
))));
|
||||
});
|
||||
Effect::new(move || {
|
||||
if remove.get() {
|
||||
let mut s = settings.get();
|
||||
s.remove_slot(signal.read().slot_id);
|
||||
reply.set(Some(HostMessage::Lobby(HostLobbyMessage::SetGameSettings(
|
||||
s,
|
||||
))));
|
||||
}
|
||||
});
|
||||
|
||||
view! { <SettingsSetupSlot setup_slot=signal players=players dialog_open=dialog_open /> }
|
||||
view! {
|
||||
<SettingsSetupSlot
|
||||
remove=remove.write_only()
|
||||
setup_slot=signal
|
||||
players=players
|
||||
dialog_open=dialog_open
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
};
|
||||
|
|
@ -124,9 +143,67 @@ pub fn Settings(
|
|||
})
|
||||
.collect_view();
|
||||
|
||||
let fill_villagers = move |ev: MouseEvent| {
|
||||
ev.prevent_default();
|
||||
let mut s = settings.get();
|
||||
s.fill_remaining_slots_with_villagers(players.read().len());
|
||||
reply.set(Some(HostMessage::Lobby(HostLobbyMessage::SetGameSettings(
|
||||
s,
|
||||
))));
|
||||
};
|
||||
let clear_setup = move |ev: MouseEvent| {
|
||||
ev.prevent_default();
|
||||
reply.set(Some(HostMessage::Lobby(HostLobbyMessage::SetGameSettings(
|
||||
GameSettings::empty(),
|
||||
))));
|
||||
};
|
||||
let fill_disabled = move || settings.read().slots().len() >= players.read().len();
|
||||
let clear_disabled = move || settings.read().slots().is_empty();
|
||||
|
||||
let start_content = move || {
|
||||
if let Err(err) = settings.read().check_with_player_list(
|
||||
&players
|
||||
.read()
|
||||
.iter()
|
||||
.map(|p| p.identification.clone())
|
||||
.collect::<Box<_>>(),
|
||||
) {
|
||||
return view! { <span class="error">{err.to_string()}</span> }.into_any();
|
||||
}
|
||||
let start = move |ev: MouseEvent| {
|
||||
ev.prevent_default();
|
||||
reply.set(Some(HostMessage::Lobby(HostLobbyMessage::Start)));
|
||||
};
|
||||
view! {
|
||||
<DialogModal
|
||||
button_class="start-game".into()
|
||||
button_content="start game"
|
||||
mode=DialogMode::Box
|
||||
>
|
||||
<h3>"start game"</h3>
|
||||
<span class="start-game-dialog-info">
|
||||
"once the game starts, all players will recieve their role on their phones"
|
||||
</span>
|
||||
<span class="start-game-dialog-info">
|
||||
"instruct them to ensure no other player can see their screen at this time"
|
||||
</span>
|
||||
<button on:click=start>"start game"</button>
|
||||
</DialogModal>
|
||||
}
|
||||
.into_any()
|
||||
// }.into_any()
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="game-settings">
|
||||
<div class="top-bar">{qr_mode_btn}</div>
|
||||
<div class="top-bar">
|
||||
{qr_mode_btn} <button on:click=fill_villagers disabled=fill_disabled>
|
||||
"fill slots with villagers"
|
||||
</button> <button on:click=clear_setup disabled=clear_disabled>
|
||||
"clear setup"
|
||||
</button>
|
||||
</div>
|
||||
<div class="start-game-status">{start_content}</div>
|
||||
<div class="role-add-list">{categories}</div>
|
||||
<div class="setup-slots">{slots}</div>
|
||||
</div>
|
||||
|
|
@ -138,6 +215,7 @@ fn SettingsSetupSlot(
|
|||
setup_slot: RwSignal<SetupSlot>,
|
||||
players: ReadSignal<Box<[PlayerState]>>,
|
||||
dialog_open: RwSignal<HashMap<SlotId, bool>>,
|
||||
remove: WriteSignal<bool>,
|
||||
) -> impl IntoView {
|
||||
let auras = move || {
|
||||
let slot = setup_slot.read();
|
||||
|
|
@ -215,7 +293,7 @@ fn SettingsSetupSlot(
|
|||
button_content=setup_slot.read().role.title().to_string().to_case(Case::Title)
|
||||
close_backdrop=true
|
||||
>
|
||||
<SlotSettingsDialogBody setup_slot=setup_slot players=players />
|
||||
<SlotSettingsDialogBody remove=remove setup_slot=setup_slot players=players />
|
||||
</DialogModal>
|
||||
{assigned_to}
|
||||
{auras}
|
||||
|
|
@ -228,6 +306,7 @@ fn SettingsSetupSlot(
|
|||
fn SlotSettingsDialogBody(
|
||||
setup_slot: RwSignal<SetupSlot>,
|
||||
players: ReadSignal<Box<[PlayerState]>>,
|
||||
remove: WriteSignal<bool>,
|
||||
) -> impl IntoView {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
enum OpenTab {
|
||||
|
|
@ -257,6 +336,10 @@ fn SlotSettingsDialogBody(
|
|||
.map(|p| p.identification.public)
|
||||
.map(|id| view! { <IdentityInline ident=RwSignal::new(id).read_only() /> }.into_any())
|
||||
.unwrap_or_else(|| view! { "none" }.into_any());
|
||||
let remove = move |ev: MouseEvent| {
|
||||
ev.prevent_default();
|
||||
remove.set(true);
|
||||
};
|
||||
view! {
|
||||
<span class=[
|
||||
"role-title",
|
||||
|
|
@ -288,6 +371,7 @@ fn SlotSettingsDialogBody(
|
|||
</div>
|
||||
</div>
|
||||
<div class="tab-content">{tab_view}</div>
|
||||
<button on:click=remove>"remove role"</button>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,11 @@ pub fn PlayerLobby(
|
|||
form_hidden.set(true);
|
||||
};
|
||||
view! {
|
||||
<form class="number-update bigger" on:submit=submit hidden=move || form_hidden.get()>
|
||||
<form
|
||||
class="number-update bigger"
|
||||
on:submit=submit
|
||||
hidden=move || form_hidden.get()
|
||||
>
|
||||
<label for="player-number">"change seat number"</label>
|
||||
<input
|
||||
id="player-number"
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ impl GameDatabase {
|
|||
let new_game =
|
||||
Game::new_with_assigned_character_ids(&with_char_ids, settings.clone())?;
|
||||
|
||||
record.game_state = GameRecordState::Started(new_game);
|
||||
record.game_state = GameRecordState::RoleReveal(new_game);
|
||||
}
|
||||
GameRecordState::GameOver(_)
|
||||
| GameRecordState::RoleReveal(_)
|
||||
|
|
@ -136,7 +136,10 @@ impl GameDatabase {
|
|||
E: ::sqlx::Executor<'a, Database = Postgres>,
|
||||
{
|
||||
let game_state_json = leptos::serde_json::to_value(&record.game_state)
|
||||
.map_err(|err| DatabaseError::Serialization(err.to_string()))?;
|
||||
.map_err(|err| DatabaseError::Serialization(err.to_string()))
|
||||
.inspect_err(|err| {
|
||||
log::error!("serializing game state: {:?}: {err}", record.game_state)
|
||||
})?;
|
||||
let game_status = match &record.game_state {
|
||||
GameRecordState::Lobby(_) => "Lobby",
|
||||
GameRecordState::RoleReveal(_) => "RoleReveal",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ mod ssr {
|
|||
};
|
||||
use futures::StreamExt;
|
||||
use leptos::prelude::RenderHtml;
|
||||
|
||||
|
||||
use tokio::io::AsyncReadExt;
|
||||
use werewolves::db::AppState;
|
||||
|
||||
pub async fn server_fn_handler(
|
||||
|
|
@ -50,7 +51,24 @@ mod ssr {
|
|||
None
|
||||
}
|
||||
}) {
|
||||
file
|
||||
if option_env!("LEPTOS_WATCH") == Some("true") {
|
||||
let file_path = std::path::PathBuf::from(".")
|
||||
.join("target")
|
||||
.join("site")
|
||||
.join(path);
|
||||
if let Ok(mut f) = tokio::fs::File::open(file_path).await {
|
||||
let mut content = Vec::new();
|
||||
if f.read_to_end(&mut content).await.is_ok() {
|
||||
content
|
||||
} else {
|
||||
file.to_vec()
|
||||
}
|
||||
} else {
|
||||
file.to_vec()
|
||||
}
|
||||
} else {
|
||||
file.to_vec()
|
||||
}
|
||||
} else {
|
||||
leptos_meta::provide_meta_context();
|
||||
let opts = state.leptos_options.clone();
|
||||
|
|
|
|||
|
|
@ -32,18 +32,12 @@ type Result<T> = core::result::Result<T, GameError>;
|
|||
pub struct GameRunner<'a> {
|
||||
game_id: GameId,
|
||||
game: &'a mut Game,
|
||||
roles_revealed: bool,
|
||||
db: Database,
|
||||
}
|
||||
|
||||
impl<'a> GameRunner<'a> {
|
||||
pub fn new(game_id: GameId, game: &'a mut Game, db: Database) -> Self {
|
||||
Self {
|
||||
db,
|
||||
game,
|
||||
game_id,
|
||||
roles_revealed: false,
|
||||
}
|
||||
Self { db, game, game_id }
|
||||
}
|
||||
|
||||
pub fn game_over(&self) -> Option<GameOver> {
|
||||
|
|
@ -232,13 +226,11 @@ impl<'a> GameRunner<'a> {
|
|||
}
|
||||
|
||||
pub fn host_message(&mut self, message: HostMessage) -> Result<ServerToHostMessage> {
|
||||
if !self.roles_revealed {
|
||||
return Err(GameError::NeedRoleReveal);
|
||||
}
|
||||
|
||||
match message {
|
||||
HostMessage::GetState => self.game.process(HostGameMessage::GetState),
|
||||
HostMessage::InGame(msg) => self.game.process(msg),
|
||||
#[cfg(debug_assertions)]
|
||||
HostMessage::ForceAllRoleAcks => Err(GameError::GameOngoing),
|
||||
HostMessage::Lobby(_) | HostMessage::PostGame(_) | HostMessage::ForceRoleAckFor(_) => {
|
||||
Err(GameError::GameOngoing)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -219,6 +219,10 @@ impl<'a> Lobby<'a> {
|
|||
self.qr_mode = mode;
|
||||
self.send_lobby_info_to_host().await.log_debug(loc!());
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
HostOrClientMessage::Host(HostMessage::ForceAllRoleAcks) => {
|
||||
return Err(GameError::InvalidMessageForGameState.into());
|
||||
}
|
||||
HostOrClientMessage::Host(HostMessage::InGame(_))
|
||||
| HostOrClientMessage::Host(HostMessage::ForceRoleAckFor(_)) => {
|
||||
return Err(GameError::InvalidMessageForGameState.into());
|
||||
|
|
|
|||
|
|
@ -99,6 +99,17 @@ impl<'a> RoleReveal<'a> {
|
|||
/// returns `true` once every player has ack'd
|
||||
pub async fn process(&mut self, message: HostOrClientMessage) -> bool {
|
||||
match message {
|
||||
#[cfg(debug_assertions)]
|
||||
HostOrClientMessage::Host(HostMessage::ForceAllRoleAcks) => {
|
||||
let mut to_notify = Vec::new();
|
||||
for (c, ackd) in self.acks.iter_mut().filter(|(_, ackd)| !*ackd) {
|
||||
*ackd = true;
|
||||
to_notify.push(c.player_id());
|
||||
}
|
||||
for pid in to_notify {
|
||||
self.notify_of_role(pid).await;
|
||||
}
|
||||
}
|
||||
HostOrClientMessage::Host(HostMessage::ForceRoleAckFor(char_id)) => {
|
||||
if let Some((c, ackd)) = self
|
||||
.acks
|
||||
|
|
|
|||
Loading…
Reference in New Issue