integrate overrides into host nav
This commit is contained in:
parent
ec29c66e4b
commit
8e1d2e34d0
|
|
@ -22,6 +22,7 @@ use futures::{
|
||||||
use gloo::net::websocket::{self, futures::WebSocket};
|
use gloo::net::websocket::{self, futures::WebSocket};
|
||||||
use instant::Instant;
|
use instant::Instant;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use werewolves_macros::Titles;
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
|
|
@ -47,6 +48,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
pages::RolePage,
|
pages::RolePage,
|
||||||
storage::StorageKey,
|
storage::StorageKey,
|
||||||
|
test_util::TestScreens,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::WerewolfError;
|
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 {
|
pub enum HostEvent {
|
||||||
SetErrorCallback(Callback<Option<WerewolfError>>),
|
SetErrorCallback(Callback<Option<WerewolfError>>),
|
||||||
SetBigScreenState(bool),
|
SetBigScreenState(bool),
|
||||||
|
|
@ -201,8 +203,11 @@ pub enum HostEvent {
|
||||||
Settings(GameSettings),
|
Settings(GameSettings),
|
||||||
Error(GameError),
|
Error(GameError),
|
||||||
QrMode(bool),
|
QrMode(bool),
|
||||||
|
ToOverrideView,
|
||||||
|
ReturnFromOverride,
|
||||||
|
ExpectEcho(Box<HostEvent>),
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Titles)]
|
||||||
pub enum HostState {
|
pub enum HostState {
|
||||||
Disconnected,
|
Disconnected,
|
||||||
Lobby {
|
Lobby {
|
||||||
|
|
@ -224,6 +229,9 @@ pub enum HostState {
|
||||||
},
|
},
|
||||||
Prompt(ActionPrompt, usize),
|
Prompt(ActionPrompt, usize),
|
||||||
Result(Option<CharacterIdentity>, ActionResult),
|
Result(Option<CharacterIdentity>, ActionResult),
|
||||||
|
ScreenOverrides {
|
||||||
|
return_to: Box<HostState>,
|
||||||
|
},
|
||||||
Story {
|
Story {
|
||||||
story: GameStory,
|
story: GameStory,
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
|
@ -280,6 +288,7 @@ pub struct Host {
|
||||||
big_screen: bool,
|
big_screen: bool,
|
||||||
qr_mode: bool,
|
qr_mode: bool,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
|
expecting_echo: Option<HostEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Host {
|
impl Component for Host {
|
||||||
|
|
@ -310,12 +319,33 @@ impl Component for Host {
|
||||||
log::error!("{err}")
|
log::error!("{err}")
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
expecting_echo: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||||
log::trace!("state: {:?}", self.state);
|
log::trace!("state: {:?}", self.state);
|
||||||
let content = match self.state.clone() {
|
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) => {
|
HostState::CharacterStates(chars) => {
|
||||||
html! {
|
html! {
|
||||||
<CharacterStatesReadOnly states={chars}/>
|
<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 {
|
let view_roles_btn = match &self.state {
|
||||||
HostState::Prompt(_, _) | HostState::Result(_, _) => {
|
HostState::Prompt(_, _) | HostState::Result(_, _) => {
|
||||||
let on_view_click = crate::callback::send_message(
|
let on_view_click = crate::callback::send_message(
|
||||||
|
|
@ -494,12 +515,52 @@ impl Component for Host {
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => 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(|| {
|
let nav = self.big_screen.not().then(|| {
|
||||||
html! {
|
html! {
|
||||||
<nav class="host-nav" style="z-index: 3;">
|
<nav class="host-nav" style="z-index: 3;">
|
||||||
<Button on_click={on_prev_click}>{"previous"}</Button>
|
{previous_btn}
|
||||||
{view_roles_btn}
|
{view_roles_btn}
|
||||||
{debug_nav}
|
{override_screens_btn}
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -514,116 +575,8 @@ impl Component for Host {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
log::debug!("update: {msg:?}, current: {:?}", self.state);
|
log::debug!("update: {}, current: {}", msg.title(), self.state.title());
|
||||||
match msg {
|
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
|
|
||||||
}
|
|
||||||
HostEvent::Continue => {
|
HostEvent::Continue => {
|
||||||
if self.big_screen {
|
if self.big_screen {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -639,13 +592,178 @@ impl Component for Host {
|
||||||
log::error!("sending next: {err:?}")
|
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 {
|
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 {
|
fn lobby_big_screen_show_setup(&self, settings: GameSettings) -> Html {
|
||||||
if !self.qr_mode {
|
if !self.qr_mode {
|
||||||
return html! {
|
return html! {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue