integrate overrides into host nav

This commit is contained in:
emilis 2025-11-18 11:44:44 +00:00
parent ec29c66e4b
commit 8e1d2e34d0
No known key found for this signature in database
1 changed files with 242 additions and 124 deletions

View File

@ -22,6 +22,7 @@ use futures::{
use gloo::net::websocket::{self, futures::WebSocket};
use instant::Instant;
use serde::Serialize;
use werewolves_macros::Titles;
use werewolves_proto::{
character::CharacterId,
error::GameError,
@ -47,6 +48,7 @@ use crate::{
},
pages::RolePage,
storage::StorageKey,
test_util::TestScreens,
};
use crate::WerewolfError;
@ -190,7 +192,7 @@ async fn worker(mut recv: Receiver<HostMessage>, scope: Scope<Host>) {
}
}
#[derive(Debug)]
#[derive(Debug, Clone, Titles, PartialEq)]
pub enum HostEvent {
SetErrorCallback(Callback<Option<WerewolfError>>),
SetBigScreenState(bool),
@ -201,8 +203,11 @@ pub enum HostEvent {
Settings(GameSettings),
Error(GameError),
QrMode(bool),
ToOverrideView,
ReturnFromOverride,
ExpectEcho(Box<HostEvent>),
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Titles)]
pub enum HostState {
Disconnected,
Lobby {
@ -224,6 +229,9 @@ pub enum HostState {
},
Prompt(ActionPrompt, usize),
Result(Option<CharacterIdentity>, ActionResult),
ScreenOverrides {
return_to: Box<HostState>,
},
Story {
story: GameStory,
#[allow(unused)]
@ -280,6 +288,7 @@ pub struct Host {
big_screen: bool,
qr_mode: bool,
debug: bool,
expecting_echo: Option<HostEvent>,
}
impl Component for Host {
@ -310,12 +319,33 @@ impl Component for Host {
log::error!("{err}")
}
}),
expecting_echo: None,
}
}
fn view(&self, _ctx: &Context<Self>) -> Html {
log::trace!("state: {:?}", self.state);
let content = match self.state.clone() {
HostState::ScreenOverrides { .. } => {
let send = {
let send = self.send.clone();
let on_err = self.error_callback.clone();
let scope = _ctx.link().clone();
Callback::from(move |msg: ServerToHostMessage| {
scope.send_message(HostEvent::ExpectEcho(Box::new(msg.clone().into())));
let mut send = send.clone();
let on_err = on_err.clone();
yew::platform::spawn_local(async move {
if let Err(err) = send.send(HostMessage::Echo(msg)).await {
on_err.emit(Some(err.into()))
}
});
})
};
html! {
<TestScreens send={send} />
}
}
HostState::CharacterStates(chars) => {
html! {
<CharacterStatesReadOnly states={chars}/>
@ -466,15 +496,6 @@ impl Component for Host {
}
}
};
let debug_nav = self.debug.then(|| {
html! {
<a href="/host/test"><Button on_click={|_|()}>{"test screens"}</Button></a>
}
});
let on_prev_click = callback::send_message(
HostMessage::InGame(HostGameMessage::PreviousState),
self.send.clone(),
);
let view_roles_btn = match &self.state {
HostState::Prompt(_, _) | HostState::Result(_, _) => {
let on_view_click = crate::callback::send_message(
@ -494,12 +515,52 @@ impl Component for Host {
}
_ => None,
};
let override_screens_btn = match &self.state {
HostState::Prompt(_, _) | HostState::Result(_, _) => {
let overrides_click = {
let scope = _ctx.link().clone();
Callback::from(move |_| {
scope.send_message(HostEvent::ToOverrideView);
})
};
Some(html! {
<Button on_click={overrides_click}>{"overrides"}</Button>
})
}
HostState::ScreenOverrides { .. } => {
let return_click = {
let scope = _ctx.link().clone();
Callback::from(move |_| {
scope.send_message(HostEvent::ReturnFromOverride);
})
};
Some(html! {
<Button on_click={return_click}>{"back"}</Button>
})
}
_ => None,
};
let previous_btn = match &self.state {
HostState::Prompt(_, _) | HostState::Result(_, _) => {
let on_prev_click = callback::send_message(
HostMessage::InGame(HostGameMessage::PreviousState),
self.send.clone(),
);
Some(html! {
<Button on_click={on_prev_click}>{"previous"}</Button>
})
}
_ => None,
};
let nav = self.big_screen.not().then(|| {
html! {
<nav class="host-nav" style="z-index: 3;">
<Button on_click={on_prev_click}>{"previous"}</Button>
{previous_btn}
{view_roles_btn}
{debug_nav}
{override_screens_btn}
</nav>
}
});
@ -514,116 +575,8 @@ impl Component for Host {
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
log::debug!("update: {msg:?}, current: {:?}", self.state);
match msg {
HostEvent::CharacterList(char) => {
if self.big_screen {
return false;
}
self.state = HostState::CharacterStates(char);
true
}
HostEvent::QrMode(mode) => {
self.qr_mode = mode;
true
}
HostEvent::PlayerList(mut players) => {
const LAST: NonZeroU8 = NonZeroU8::new(0xFF).unwrap();
players.sort_by(|l, r| {
l.identification
.public
.number
.unwrap_or(LAST)
.cmp(&r.identification.public.number.unwrap_or(LAST))
});
match &mut self.state {
HostState::Lobby {
players: p,
settings: _,
} => *p = players.into_iter().collect(),
HostState::Story { .. }
| HostState::Disconnected
| HostState::GameOver { .. } => {
let mut send = self.send.clone();
let on_err = self.error_callback.clone();
self.state = HostState::Lobby {
players: players.into_iter().collect(),
settings: Default::default(),
};
yew::platform::spawn_local(async move {
if let Err(err) = send
.send(HostMessage::Lobby(HostLobbyMessage::GetGameSettings))
.await
{
on_err.emit(Some(err.into()))
}
});
}
HostState::CharacterStates(_)
| HostState::Prompt(_, _)
| HostState::Result(_, _)
| HostState::RoleReveal { .. }
| HostState::Day { .. } => {
return false;
}
}
true
}
HostEvent::SetErrorCallback(callback) => {
self.error_callback = callback;
false
}
HostEvent::SetState(state) => {
self.state = state;
true
}
HostEvent::Settings(settings) => match &mut self.state {
HostState::Lobby {
players: _,
settings: s,
} => {
*s = settings;
true
}
HostState::CharacterStates(_)
| HostState::Story { .. }
| HostState::Prompt(_, _)
| HostState::Result(_, _)
| HostState::Disconnected
| HostState::RoleReveal {
ackd: _,
waiting: _,
}
| HostState::GameOver { result: _ }
| HostState::Day { .. } => {
log::info!("ignoring settings get");
false
}
},
HostEvent::Error(err) => {
self.error_callback.emit(Some(WerewolfError::Game(err)));
false
}
HostEvent::SetBigScreenState(state) => {
self.big_screen = state;
if self.big_screen {
gloo::utils::document_element().set_class_name("big-screen")
}
if state {
let (mut discard_send, mut discard_recv) = futures::channel::mpsc::channel(10);
core::mem::swap(&mut discard_send, &mut self.send);
Box::leak(Box::new(discard_send));
yew::platform::spawn_local(async move {
while discard_recv.next().await.is_some() {}
});
}
self.debug = false;
self.error_callback = Callback::noop();
false
}
log::debug!("update: {}, current: {}", msg.title(), self.state.title());
match &msg {
HostEvent::Continue => {
if self.big_screen {
return false;
@ -639,13 +592,178 @@ impl Component for Host {
log::error!("sending next: {err:?}")
}
});
false
}
HostEvent::SetErrorCallback(cb) => {
self.error_callback = cb.clone();
}
HostEvent::SetBigScreenState(state) => {
self.big_screen = *state;
if self.big_screen {
gloo::utils::document_element().set_class_name("big-screen")
}
if *state {
let (mut discard_send, mut discard_recv) = futures::channel::mpsc::channel(10);
core::mem::swap(&mut discard_send, &mut self.send);
Box::leak(Box::new(discard_send));
yew::platform::spawn_local(async move {
while discard_recv.next().await.is_some() {}
});
}
self.debug = false;
self.error_callback = Callback::noop();
}
HostEvent::QrMode(mode) => {
self.qr_mode = *mode;
}
HostEvent::ExpectEcho(echo) => {
self.expecting_echo.replace((**echo).clone());
}
HostEvent::ReturnFromOverride => {
if let HostState::ScreenOverrides { return_to } = &self.state {
log::debug!("returning from screen overrides to: {}", return_to.title());
self.state = (**return_to).clone();
return true;
} else {
return false;
}
}
_ => {}
}
if let HostState::ScreenOverrides { .. } = &self.state {
let (state, update) = self.message_to_new_state(msg.clone());
if let Some(mut state) = state
// re-borrow so we can borrow self immutably for [message_to_new_state]
&& let HostState::ScreenOverrides { return_to } = &mut self.state
&& (self.expecting_echo.is_none()
|| self
.expecting_echo
.as_ref()
.map(|exp| *exp != msg)
.unwrap_or_default())
{
log::debug!("screen override: new return_to: {}", return_to.title());
core::mem::swap(&mut **return_to, &mut state);
self.expecting_echo.take();
}
return update;
}
let (state, update) = self.message_to_new_state(msg);
if let Some(state) = state {
self.state = state;
}
update
}
}
impl Host {
fn message_to_new_state(&self, msg: HostEvent) -> (Option<HostState>, bool) {
match msg {
HostEvent::ExpectEcho(_) => (None, false),
HostEvent::ReturnFromOverride => {
if let HostState::ScreenOverrides { return_to } = &self.state {
(Some((**return_to).clone()), true)
} else {
(None, false)
}
}
HostEvent::ToOverrideView => {
if let HostState::ScreenOverrides { .. } = &self.state {
return (None, false);
}
(
Some(HostState::ScreenOverrides {
return_to: Box::new(self.state.clone()),
}),
true,
)
}
HostEvent::CharacterList(char) => {
if self.big_screen {
return (None, false);
}
(Some(HostState::CharacterStates(char)), true)
}
HostEvent::QrMode(mode) => (None, true),
HostEvent::PlayerList(mut players) => {
const LAST: NonZeroU8 = NonZeroU8::new(0xFF).unwrap();
players.sort_by(|l, r| {
l.identification
.public
.number
.unwrap_or(LAST)
.cmp(&r.identification.public.number.unwrap_or(LAST))
});
match &self.state {
HostState::Lobby { settings, .. } => (
Some(HostState::Lobby {
players: players.into_iter().collect(),
settings: settings.clone(),
}),
true,
),
HostState::Story { .. }
| HostState::Disconnected
| HostState::GameOver { .. } => {
let mut send = self.send.clone();
let on_err = self.error_callback.clone();
let state = Some(HostState::Lobby {
players: players.into_iter().collect(),
settings: Default::default(),
});
yew::platform::spawn_local(async move {
if let Err(err) = send
.send(HostMessage::Lobby(HostLobbyMessage::GetGameSettings))
.await
{
on_err.emit(Some(err.into()))
}
});
(state, true)
}
HostState::ScreenOverrides { .. }
| HostState::CharacterStates(_)
| HostState::Prompt(_, _)
| HostState::Result(_, _)
| HostState::RoleReveal { .. }
| HostState::Day { .. } => (None, false),
}
}
HostEvent::SetErrorCallback(callback) => (None, false),
HostEvent::SetState(state) => (Some(state), true),
HostEvent::Settings(settings) => match &self.state {
HostState::Lobby { players, .. } => (
Some(HostState::Lobby {
settings,
players: players.clone(),
}),
true,
),
HostState::ScreenOverrides { .. }
| HostState::CharacterStates(_)
| HostState::Story { .. }
| HostState::Prompt(_, _)
| HostState::Result(_, _)
| HostState::Disconnected
| HostState::RoleReveal {
ackd: _,
waiting: _,
}
| HostState::GameOver { result: _ }
| HostState::Day { .. } => {
log::info!("ignoring settings get");
(None, false)
}
},
HostEvent::Error(err) => (None, false),
HostEvent::SetBigScreenState(state) => (None, true),
HostEvent::Continue => (None, false),
}
}
fn lobby_big_screen_show_setup(&self, settings: GameSettings) -> Html {
if !self.qr_mode {
return html! {