diff --git a/src/game/hand.rs b/src/game/hand.rs index be67921..fd5a97b 100644 --- a/src/game/hand.rs +++ b/src/game/hand.rs @@ -46,7 +46,6 @@ pub(crate) fn shuffle_deal( // commands.get_entity(*wall_ent)?.replace_children(&walltiles); for player_ent in players { - debug!("deal to player_ent {player_ent:?}"); let handtiles = walltiles.split_off(walltiles.len() - 13); // commands.get_entity(*wall_ent)?.remove_children(&handtiles); @@ -57,6 +56,7 @@ pub(crate) fn shuffle_deal( .get_entity(player_ent)? .replace_children(&[hand_ent]); + debug!("dealt to player_ent {player_ent:?}"); } commands.get_entity(*wall_ent)?.replace_children(&walltiles); diff --git a/src/game/round.rs b/src/game/round.rs index ff9f522..9cf95df 100644 --- a/src/game/round.rs +++ b/src/game/round.rs @@ -45,7 +45,7 @@ impl Default for MatchSettings { } } -#[derive(Component, Clone, Copy, FromRepr, EnumCount, PartialEq)] +#[derive(Component, Clone, Copy, FromRepr, EnumCount)] pub enum Wind { Ton, Nan, @@ -53,12 +53,6 @@ pub enum Wind { Pei, } -pub enum WindRelation { - Shimocha, - Toimen, - Kamicha, -} - impl EnumNextCycle for Wind { fn next(&self) -> Self { if (*self as usize + 1) >= Self::COUNT { @@ -69,18 +63,6 @@ impl EnumNextCycle for Wind { } } -impl Wind { - pub fn relate(&self, other: &Self) -> WindRelation { - if self.next() == *other { - WindRelation::Shimocha - } else if other.next() == *self { - WindRelation::Kamicha - } else { - WindRelation::Toimen - } - } -} - #[derive(Resource)] pub(crate) struct CurrentPlayer(pub Entity); @@ -115,7 +97,7 @@ pub(crate) fn tsumo( curr_turnstate: Res>, mut next_turnstate: ResMut>, ) { - debug!("tsumo for: {:?}", curr_player.0); + trace!("tsumo for: {:?}", curr_player.0); let drawn = walltiles.last().unwrap(); commands.entity(*wall_ent).remove_child(*drawn); diff --git a/src/main.rs b/src/main.rs index 784bd0a..aa4e2ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,8 +41,7 @@ fn main() { tui_logger::init_logger(tui_logger::LevelFilter::Trace).unwrap(); tui_logger::set_env_filter_from_string(FILTERSTRING); - // app.add_plugins(tui::RiichiTui) - app.add_plugins(tui::TuiPlugin) + app.add_plugins(tui::RiichiTui) } }; diff --git a/src/tui.rs b/src/tui.rs deleted file mode 100644 index f577ca4..0000000 --- a/src/tui.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::time::Duration; - -use bevy::{app::ScheduleRunnerPlugin, prelude::*, state::app::StatesPlugin}; -use bevy_ratatui::RatatuiPlugins; -use jong::game::GameState; -use tui_logger::TuiWidgetState; - -use crate::tui::states::ConsoleWidget; - -mod input; -mod layout; -mod render; - -#[derive(Default)] -pub struct TuiPlugin; - -#[derive(Clone, Debug, Eq, Hash, PartialEq, SystemSet)] -pub enum TuiSet { - Input, - Layout, - Render, -} - -impl Plugin for TuiPlugin { - fn build(&self, app: &mut App) { - app.add_plugins(( - MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f32( - 1. / 60., - ))), - RatatuiPlugins { - // enable_kitty_protocol: todo!(), - enable_mouse_capture: true, - enable_input_forwarding: true, - ..Default::default() - }, - )) - .add_plugins(StatesPlugin) - .init_resource::() - .insert_resource(ConsoleWidget { - state: TuiWidgetState::new(), - open: true, - }) - .init_state::() - .configure_sets( - Update, - (TuiSet::Input, TuiSet::Layout, TuiSet::Render).chain(), - ) - .add_systems( - Update, - (input::keyboard, input::mouse).in_set(TuiSet::Input), - ) - .add_systems(Update, layout::layout.in_set(TuiSet::Layout)) - .add_systems( - Update, - ( - render::render_hands.run_if(in_state(GameState::Play)), - render::render, - ) - .chain() - .in_set(TuiSet::Render), - ); - } -} - -mod states { - use bevy::prelude::*; - use tui_logger::TuiWidgetState; - - #[derive(Resource)] - pub(crate) struct ConsoleWidget { - pub(crate) state: TuiWidgetState, - pub(crate) open: bool, - } - - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default)] - pub(crate) enum TuiState { - #[default] - MainMenu, - InGame, - } - - // #[derive(SubStates, Default, Clone, Copy, PartialEq, Eq, Hash, Debug)] - // #[source(TuiState = TuiState::MainMenu)] - // pub(crate) enum ZenState { - // #[default] - // Menu, - // Zen, - // } -} diff --git a/src/tui/console.rs b/src/tui/console.rs new file mode 100644 index 0000000..7cd2171 --- /dev/null +++ b/src/tui/console.rs @@ -0,0 +1,34 @@ +use bevy::prelude::*; +use ratatui::widgets::{Block, Clear}; +use tui_logger::TuiLoggerWidget; + +use crate::tui::render::WidgetStack; + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +pub(crate) enum ConsoleState { + #[default] + Closed, + Open, +} + +impl std::ops::Not for ConsoleState { + type Output = Self; + + fn not(self) -> Self::Output { + match self { + ConsoleState::Open => ConsoleState::Closed, + ConsoleState::Closed => ConsoleState::Open, + } + } +} + +pub(crate) fn draw_console(mut widgets: ResMut) { + widgets.0.push(Box::new(|frame| { + let block = Block::bordered().title("console"); + frame.render_widget(Clear, frame.area()); + frame.render_widget( + TuiLoggerWidget::default().block(block), + frame.area(), /* .inner(Margin { horizontal: 8, vertical: 8 }) */ + ); + })); +} diff --git a/src/tui/input.rs b/src/tui/input.rs deleted file mode 100644 index 915c3bb..0000000 --- a/src/tui/input.rs +++ /dev/null @@ -1,149 +0,0 @@ -pub(crate) use keyboard::keyboard; -pub(crate) use mouse::mouse; - -pub(crate) mod keyboard { - use bevy::prelude::*; - use bevy_ratatui::event::KeyMessage; - use ratatui::crossterm::event::KeyCode; - use tui_logger::TuiWidgetEvent; - - use jong::game::GameState; - - use crate::tui::layout::Overlays; - use crate::tui::states::ConsoleWidget; - use crate::tui::states::TuiState; - - pub(crate) fn keyboard( - mut messages: MessageReader, - mut overlays: ResMut, - mut consolewidget: ResMut, - mut exit: MessageWriter, - - curr_gamestate: Res>, - curr_tuistate: Res>, - - mut next_gamestate: ResMut>, - mut next_tuistate: ResMut>, - ) { - 'message: for message in messages.read() { - let key = message.code; - if consolewidget.handle_keyboard(key) { - continue 'message; - } - for overlay in overlays.stack.iter().rev() { - let consumed = match overlay { - _ => false, - }; - if consumed { - continue 'message; - } - } - - match curr_tuistate.get() { - TuiState::MainMenu => match key { - KeyCode::Char('p') => { - next_tuistate.set(TuiState::InGame); - next_gamestate.set(GameState::Setup); - } - KeyCode::Char('z') => { - // if let Some(ref curr_zenstate) = curr_zenstate { - // match curr_zenstate.get() { - // ZenState::Menu => next_zenstate.set(ZenState::Zen), - // ZenState::Zen => next_zenstate.set(ZenState::Menu), - // } - // } - } - KeyCode::Char('q') => { - exit.write_default(); - } - _ => {} - }, - TuiState::InGame => todo!(), - } - } - } - - impl ConsoleWidget { - pub(crate) fn handle_keyboard(&mut self, key: KeyCode) -> bool { - if key == KeyCode::Char('`') { - self.open = !self.open; - return true; - } - if self.open { - match key { - KeyCode::Up => self.state.transition(TuiWidgetEvent::UpKey), - KeyCode::Down => self.state.transition(TuiWidgetEvent::DownKey), - // KeyCode::Home => self.state.transition(TuiWidgetEvent::), - // KeyCode::End => self.state.transition(TuiWidgetEvent::), - KeyCode::PageUp => self.state.transition(TuiWidgetEvent::PrevPageKey), - KeyCode::PageDown => self.state.transition(TuiWidgetEvent::NextPageKey), - KeyCode::Esc => { - self.open = false; - return true; - } - _ => return false, - } - } - self.open - } - } -} - -pub(crate) mod mouse { - use bevy::prelude::*; - use bevy_ratatui::event::MouseMessage; - use ratatui::layout::Position; - - use crate::tui::render::{Hovered, PickRegion}; - - pub(crate) fn mouse( - mut commands: Commands, - mut messages: MessageReader, - entities: Query<(Entity, &PickRegion)>, - hovered: Query<(Entity, &PickRegion), With>, - ) -> Result { - for message in messages.read() { - let event = message.0; - // let term_size = context.size().unwrap(); - let position = Position::new(event.column, event.row); - match event.kind { - ratatui::crossterm::event::MouseEventKind::Down(mouse_button) => match mouse_button - { - ratatui::crossterm::event::MouseButton::Left => { - for (_entity, _region) in &entities {} - } - // ratatui::crossterm::event::MouseButton::Right => todo!(), - // ratatui::crossterm::event::MouseButton::Middle => todo!(), - _ => {} - }, - // ratatui::crossterm::event::MouseEventKind::Up(mouse_button) => todo!(), - // ratatui::crossterm::event::MouseEventKind::Drag(mouse_button) => todo!(), - ratatui::crossterm::event::MouseEventKind::Moved => { - for (entity, region) in &hovered { - if !region.area.contains(position) { - commands.get_entity(entity)?.remove::(); - } - } - for (entity, region) in &entities { - // debug!( - // "{:?}, {position:?}", - // // region.area.positions().collect::>() - // region.area - // ); - if region.area.contains(position) { - commands.get_entity(entity)?.insert(Hovered); - // trace!("{entity:?} hovered!") - } - } - } - // ratatui::crossterm::event::MouseEventKind::ScrollDown => todo!(), - // ratatui::crossterm::event::MouseEventKind::ScrollUp => todo!(), - // ratatui::crossterm::event::MouseEventKind::ScrollLeft => todo!(), - // ratatui::crossterm::event::MouseEventKind::ScrollRight => todo!(), - _ => {} - } - } - - Ok(()) - } -} diff --git a/src/tui/input/keyboard.rs b/src/tui/input/keyboard.rs new file mode 100644 index 0000000..6e70e66 --- /dev/null +++ b/src/tui/input/keyboard.rs @@ -0,0 +1,91 @@ +use bevy::prelude::*; +use bevy_ratatui::crossterm::event::KeyCode; +use bevy_ratatui::event::KeyMessage; + +use jong::game::GameState; + +use crate::tui::{TuiState, ZenState, console::ConsoleState}; + +// TODO change this to handle console open request, esc for menu, etc, then +// route other messages to other systems + +#[allow(clippy::too_many_arguments)] +pub(crate) fn input_system( + mut messages: MessageReader, + + curr_tuistate: Res>, + curr_consolestate: Res>, + curr_gamestate: Res>, + curr_zenstate: Option>>, + + mut next_tuistate: ResMut>, + mut next_consolestate: ResMut>, + mut next_gamestate: ResMut>, + mut next_zenstate: ResMut>, + + mut exit: MessageWriter, +) { + let (ts, cs, gs) = ( + curr_tuistate.get(), + curr_consolestate.get(), + curr_gamestate.get(), + ); + + for message in messages.read() { + if let KeyCode::Char('`') = message.code { + next_consolestate.set(!*curr_consolestate.get()); + continue; + } + + if *cs == ConsoleState::Open { + let mut passthrough = false; + match message.code { + KeyCode::Up => todo!(), + KeyCode::Down => todo!(), + KeyCode::Home => todo!(), + KeyCode::End => todo!(), + KeyCode::PageUp => todo!(), + KeyCode::PageDown => todo!(), + KeyCode::Esc => next_consolestate.set(ConsoleState::Closed), + _ => passthrough = true, + } + if !passthrough { + continue; + } + } + + match ts { + TuiState::MainMenu => match message.code { + KeyCode::Char('p') => { + next_tuistate.set(TuiState::InGame); + next_gamestate.set(GameState::Setup); + } + KeyCode::Char('z') => { + if let Some(ref curr_zenstate) = curr_zenstate { + match curr_zenstate.get() { + ZenState::Menu => next_zenstate.set(ZenState::Zen), + ZenState::Zen => next_zenstate.set(ZenState::Menu), + } + } + } + KeyCode::Char('q') => { + exit.write_default(); + } + _ => {} + }, + TuiState::InGame => match gs { + GameState::Setup => match message.code { + _ => {} + }, + GameState::Play => match message.code { + KeyCode::Char('q') => { + exit.write_default(); + } + _ => {} + }, + _ => todo!(), + _ => unreachable!("TuiState::InGame but GameState invalid"), + }, + } + } +} diff --git a/src/tui/input/mod.rs b/src/tui/input/mod.rs new file mode 100644 index 0000000..d7eb1ae --- /dev/null +++ b/src/tui/input/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod keyboard; +pub(crate) mod mouse; diff --git a/src/tui/input/mouse.rs b/src/tui/input/mouse.rs new file mode 100644 index 0000000..acc9fa0 --- /dev/null +++ b/src/tui/input/mouse.rs @@ -0,0 +1,65 @@ +use bevy::prelude::*; +use bevy_ratatui::{RatatuiContext, event::MouseMessage}; +use ratatui::layout::Position; + +use crate::tui::render::Hovered; + +#[derive(Component)] +pub(crate) struct PickRegion { + pub(crate) area: ratatui::prelude::Rect, +} + +// enum PickEvent { +// Click { col: u16, row: u16 }, +// Hover { col: u16, row: u16 }, +// } + +pub(crate) fn input_system( + mut commands: Commands, + mut messages: MessageReader, + _context: Res, + entities: Query<(Entity, &PickRegion)>, + hovered: Query<(Entity, &PickRegion), With>, +) -> Result { + for message in messages.read() { + let event = message.0; + // let term_size = context.size().unwrap(); + let position = Position::new(event.column, event.row); + match event.kind { + ratatui::crossterm::event::MouseEventKind::Down(mouse_button) => match mouse_button { + ratatui::crossterm::event::MouseButton::Left => { + for (_entity, _region) in &entities {} + } + // ratatui::crossterm::event::MouseButton::Right => todo!(), + // ratatui::crossterm::event::MouseButton::Middle => todo!(), + _ => {} + }, + // ratatui::crossterm::event::MouseEventKind::Up(mouse_button) => todo!(), + // ratatui::crossterm::event::MouseEventKind::Drag(mouse_button) => todo!(), + ratatui::crossterm::event::MouseEventKind::Moved => { + for (entity, region) in &hovered { + if !region.area.contains(position) { + commands.get_entity(entity)?.remove::(); + } + } + for (entity, region) in &entities { + // debug!( + // "{:?}, {position:?}", + // region.area.positions().collect::>() + // ); + if region.area.contains(position) { + commands.get_entity(entity)?.insert(Hovered); + // trace!("{entity:?} hovered!") + } + } + } + // ratatui::crossterm::event::MouseEventKind::ScrollDown => todo!(), + // ratatui::crossterm::event::MouseEventKind::ScrollUp => todo!(), + // ratatui::crossterm::event::MouseEventKind::ScrollLeft => todo!(), + // ratatui::crossterm::event::MouseEventKind::ScrollRight => todo!(), + _ => {} + } + } + + Ok(()) +} diff --git a/src/tui/layout.rs b/src/tui/layout.rs deleted file mode 100644 index 827c6ea..0000000 --- a/src/tui/layout.rs +++ /dev/null @@ -1,82 +0,0 @@ -use bevy::prelude::*; -use bevy_ratatui::RatatuiContext; -use ratatui::{ - layout::{Constraint, Layout, Margin}, - prelude::Rect, -}; - -#[derive(Resource, Default)] -pub(crate) struct Overlays { - pub(crate) stack: Vec, -} - -pub(crate) enum Overlay {} - -#[derive(Resource, Clone, Copy)] -pub(crate) struct HandLayouts { - pub(crate) left_pond: Rect, - pub(crate) cross_pond: Rect, - pub(crate) right_pond: Rect, - pub(crate) this_pond: Rect, - pub(crate) left_hand: Rect, - pub(crate) cross_hand: Rect, - pub(crate) right_hand: Rect, - pub(crate) this_hand: Rect, -} - -pub(crate) enum LayoutError { - TerminalTooSmall, -} - -pub(crate) fn layout(mut commands: Commands, mut tui: ResMut) -> Result { - tui.autoresize()?; - let frame = tui.get_frame(); - let term_area = frame.area(); - - let tiles = tiles_areas(term_area); - commands.insert_resource(tiles); - - Ok(()) -} - -// TODO make fallible to warn area too small -fn tiles_areas(term_area: Rect) -> HandLayouts { - let constraints = [Constraint::Fill(3), Constraint::Fill(18)]; - let horizontal_slicer_right = Layout::horizontal(constraints); - let vertical_slicer_bottom = Layout::vertical(constraints); - let horizontal_slicer_left = Layout::horizontal(constraints.iter().rev()); - let vertical_slicer_top = Layout::vertical(constraints.iter().rev()); - let [left_hand, this_hand] = horizontal_slicer_right.areas::<2>(term_area); - let [_, left_hand] = vertical_slicer_bottom.areas::<2>(left_hand); - let [_, this_hand] = vertical_slicer_top.areas::<2>(this_hand); - let [cross_hand, right_hand] = horizontal_slicer_left.areas::<2>(term_area); - let [right_hand, _] = vertical_slicer_top.areas::<2>(right_hand); - let [cross_hand, _] = vertical_slicer_bottom.areas::<2>(cross_hand); - - let margin = Margin::new( - (left_hand.width + right_hand.width) / 2, - (cross_hand.height + this_hand.height) / 2, - ); - let pond_area = term_area.inner(margin); - let all_pond = Layout::horizontal([Constraint::Fill(1); 3]); - let cross_pond = - Layout::vertical([Constraint::Fill(1), Constraint::Max(1), Constraint::Fill(1)]); - let [mut left_pond, center, mut right_pond] = all_pond.areas::<3>(pond_area); - let [cross_pond, _compass, this_pond] = cross_pond.areas::<3>(center); - // let shift = left_pond.height - cross_pond.height; - left_pond.height = cross_pond.height; - left_pond.y += cross_pond.height / 2; - right_pond.height = cross_pond.height; - right_pond.y += cross_pond.height / 2; - - HandLayouts { - left_pond, - cross_pond, - right_pond, - this_pond, - left_hand, - cross_hand, - right_hand, - this_hand, - } -} diff --git a/src/tui/mod.rs b/src/tui/mod.rs new file mode 100644 index 0000000..1b3a25f --- /dev/null +++ b/src/tui/mod.rs @@ -0,0 +1,95 @@ +use std::time::Duration; + +use bevy::{app::ScheduleRunnerPlugin, prelude::*, state::app::StatesPlugin}; +use bevy_ratatui::RatatuiPlugins; + +use jong::game::GameState; + +use crate::tui::render::{WidgetStack, menu::Splash}; +// use jong::game::wall::InWall; + + +mod console; +mod render; +mod input; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, States, Default)] +pub(crate) enum TuiState { + #[default] + MainMenu, + InGame, +} + +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +struct InGame; + +impl ComputedStates for InGame { + type SourceStates = TuiState; + + fn compute(sources: Self::SourceStates) -> Option { + match sources { + TuiState::InGame => Some(Self), + _ => None, + } + } +} + +#[derive(SubStates, Default, Clone, Copy, PartialEq, Eq, Hash, Debug,)] +#[source(TuiState = TuiState::MainMenu)] +pub(crate) enum ZenState { + #[default] + Menu, + Zen, +} + +#[derive(Default)] +pub struct RiichiTui; +impl Plugin for RiichiTui { + fn build(&self, app: &mut App) { + app.add_plugins(( + MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f32( + 1. / 60., + ))), + RatatuiPlugins { + // enable_kitty_protocol: todo!(), + enable_mouse_capture: true, + enable_input_forwarding: true, + ..Default::default() + }, + )) + .add_plugins(StatesPlugin) + + // general setup + .init_state::() + .add_computed_state::() + .init_resource::() + .add_sub_state::() + .init_resource::() + .init_state::() + .add_systems(PostUpdate, console::draw_console.run_if(in_state(console::ConsoleState::Open))) + + // input + .add_systems(PreUpdate, input::keyboard::input_system) + .add_systems(PreUpdate, input::mouse::input_system) + + // main menu + .add_systems(PostStartup, render::menu::init_splash) + .insert_resource(Time::::from_seconds(1.0)) + .add_systems(FixedUpdate, render::menu::render_splash.run_if(in_state(TuiState::MainMenu))) + .add_systems(Update, render::menu::draw_splash.run_if(in_state(TuiState::MainMenu))) + .add_systems(Update, render::menu::draw_mainmenu.after(render::menu::draw_splash).run_if(in_state(TuiState::MainMenu).and(in_state(ZenState::Menu)))) + + // gaming + .init_resource::() + .add_systems(Update, render::hand::render_hands.run_if(in_state(InGame).and(in_state(GameState::Play)))) + .add_systems(Update, render::ingame::draw_ingame.run_if(in_state(InGame))) + + // render + .add_systems(Last, render::draw_system.run_if(not(in_state(InGame)))) + + // semicolon stopper + ; + } +} + + diff --git a/src/tui/render.rs b/src/tui/render.rs deleted file mode 100644 index ed61c4e..0000000 --- a/src/tui/render.rs +++ /dev/null @@ -1,205 +0,0 @@ -use std::io::Write as _; -use std::ops::Sub; - -use bevy::platform::collections::HashSet; -use bevy::prelude::*; -use bevy_ratatui::RatatuiContext; -use ratatui::layout::{Constraint, Flex, Layout, Offset, Rect, Size}; -use ratatui::style::{Modifier, Style, Stylize}; -use ratatui::widgets::{Block, Borders, Clear, Paragraph}; - -use jong::game::hand::{DrawnTile, Hand}; -use jong::game::player::{MainPlayer, Player}; -use jong::game::round::Wind; -use jong::tile::Tile; - -use crate::tui::layout::*; -use crate::tui::states::ConsoleWidget; - -#[derive(Component)] -pub(crate) struct Hovered; - -#[derive(Component)] -pub(crate) struct PickRegion { - pub(crate) area: Rect, -} - -fn render_tile(tile: &Tile, hovered: bool) -> (Paragraph<'_>) { - let block = ratatui::widgets::Block::bordered(); - let mut widget = Paragraph::new(match &tile.suit { - jong::tile::Suit::Pin(rank) => format!("{}\np", rank.0), - jong::tile::Suit::Sou(rank) => format!("{}\ns", rank.0), - jong::tile::Suit::Man(rank) => format!("{}\nm", rank.0), - jong::tile::Suit::Wind(wind) => (match wind { - jong::tile::Wind::Ton => "e\nw", - jong::tile::Wind::Nan => "s\nw", - jong::tile::Wind::Shaa => "w\nw", - jong::tile::Wind::Pei => "n\nw", - }) - .into(), - jong::tile::Suit::Dragon(dragon) => (match dragon { - jong::tile::Dragon::Haku => "w\nd", - jong::tile::Dragon::Hatsu => "g\nd", - jong::tile::Dragon::Chun => "r\nd", - }) - .into(), - }) - .block(block) - .centered(); - - if hovered { - widget = widget.add_modifier(Modifier::BOLD); - } - - widget -} - -fn debug_blocks(layouts: HandLayouts, frame: &mut ratatui::Frame<'_>) { - let debug_block = Block::new().borders(Borders::ALL); - frame.render_widget(debug_block.clone(), layouts.this_hand); - frame.render_widget(debug_block.clone(), layouts.left_hand); - frame.render_widget(debug_block.clone(), layouts.cross_hand); - frame.render_widget(debug_block.clone(), layouts.right_hand); - frame.render_widget(debug_block.clone(), layouts.this_pond); - frame.render_widget(debug_block.clone(), layouts.left_pond); - frame.render_widget(debug_block.clone(), layouts.cross_pond); - frame.render_widget(debug_block.clone(), layouts.right_pond); -} - -pub(crate) fn render( - mut tui: ResMut, - layouts: Res, - overlays: Res, - consolewidget: Res, -) -> Result { - let mut frame = tui.get_frame(); - - for overlay in overlays.stack.iter() { - match overlay { - _ => {} - } - } - - if consolewidget.open { - let block = Block::bordered().title("console [press esc to close]"); - frame.render_widget(Clear, frame.area()); - frame.render_widget( - tui_logger::TuiLoggerWidget::default() - .block(block) - .state(&consolewidget.state), - frame.area(), /* .inner(Margin { horizontal: 8, vertical: 8 }) */ - ); - } - - tui.hide_cursor()?; - tui.flush()?; - tui.swap_buffers(); - tui.backend_mut().flush()?; - Ok(()) -} - -pub(crate) fn render_hands( - mut commands: Commands, - mut tui: ResMut, - hovered: Query>, - layouts: Res, - mainplayer: Single<(&Player, &Wind), With>, - players: Populated<(&Player, /* &Wind, */ &Children)>, - hands: Populated<&Children, (With, Without)>, - tiles: Populated<&Tile>, - drawn_tile: Option>, -) -> Result { - let mut frame = tui.get_frame(); - debug_blocks(*layouts, &mut frame); - - for (player, /* wind, */ hand_ent) in players { - let hand = hand_ent.iter().next().unwrap(); - let hand: Vec<_> = hands - .get(hand)? - .iter() - .map(|entity| -> Result<_> { - let tile = tiles.get(entity)?; - // let paragraph = render_tile(tile,); - let hovered = hovered.contains(entity); - let widget = render_tile(tile, hovered); - Ok((entity, widget, hovered)) - }) - .collect::>()?; - - if player == mainplayer.0 { - // split main box into thirds - let mut this_hand = layouts.this_hand; - let this_drawer = drawn_tile.as_ref().is_some_and(|dt| dt.0 == player); - let tile_drawn = if this_drawer { 7 } else { 0 }; - let hand_draw_meld = Layout::horizontal([ - Constraint::Max(hand.len() as u16 * 5), - Constraint::Max(tile_drawn), - Constraint::Fill(1), - ]) - .flex(Flex::SpaceBetween); - this_hand = this_hand.offset(Offset { - x: 0, - y: this_hand.height.abs_diff(5) as i32 + 1, - }); - this_hand = this_hand.resize(Size { - width: this_hand.width, - height: 4, - }); - let [hand_area, drawn_area, meld_area] = hand_draw_meld.areas::<3>(this_hand); - - // split hand area into tile areas - let mut constraints = vec![Constraint::Max(5); hand.len()]; - constraints.push(Constraint::Fill(1)); - let layout = Layout::horizontal(constraints).flex(Flex::Start); - let tile_areas = layout.split(hand_area); - - for ((entity, widget, hovered), mut area) in - hand.into_iter().zip(tile_areas.iter().copied()) - { - if hovered { - area = area.offset(Offset { x: 0, y: -1 }); - let mut hitbox = area.as_size(); - hitbox.height += 1; - commands.entity(entity).insert(PickRegion { - area: area.resize(hitbox), - }); - } else { - commands.entity(entity).insert(PickRegion { area }); - } - frame.render_widget(widget, area); - } - - // tsumo tile - if this_drawer { - let (_, tile, entity) = **drawn_tile.as_ref().unwrap(); - let mut area = drawn_area.resize(Size { - width: 5, - height: 4, - }); - area = area.offset(Offset { x: 2, y: 0 }); - let hovered = hovered.contains(entity); - let widget = render_tile(tiles.get(tile.0)?, hovered); - if hovered { - area = area.offset(Offset { x: 0, y: -1 }); - let mut hitbox = area.as_size(); - hitbox.height += 1; - commands.entity(entity).insert(PickRegion { - area: area.resize(hitbox), - }); - } else { - commands.entity(entity).insert(PickRegion { area }); - } - frame.render_widget(widget, area); - } - // TODO draw melds - } else { - // match mainplayer.1.relate(wind) { - // jong::game::round::WindRelation::Shimocha => todo!(), - // jong::game::round::WindRelation::Toimen => todo!(), - // jong::game::round::WindRelation::Kamicha => todo!(), - // } - } - } - - Ok(()) -} diff --git a/src/tui/render/hand.rs b/src/tui/render/hand.rs new file mode 100644 index 0000000..944aa56 --- /dev/null +++ b/src/tui/render/hand.rs @@ -0,0 +1,45 @@ +use bevy::{platform::collections::HashMap, prelude::*}; + +use jong::{ + game::{hand::Hand, player::Player}, + tile::Tile, +}; + +use crate::tui::render::tile::{self, RenderedTile}; + +#[derive(Resource, Default)] +pub(crate) struct RenderedHand(pub(crate) HashMap>); + +#[allow(clippy::type_complexity)] +pub(crate) fn render_hands( + mut commands: Commands, + mut rendered_hand: ResMut, + tiles: Populated<&Tile>, + player_hands: Populated<(Entity, &Children), (With, Changed)>, + hands: Populated<&Children, (Changed, Without)>, +) -> Result { + let mut rendered = HashMap::new(); + + for (player_ent, hand) in player_hands { + let hand = hand.iter().next().unwrap(); + let tiles = hands + .get(hand)? + .iter() + .map(|it| { + tiles + .get(it) + // .inspect(|t| debug!("{t:?}")) + .map(tile::draw_tile) + .map(|p| commands.spawn(RenderedTile(p)).id()) + // .inspect(|e| debug!("{e:?}")) + .unwrap() + }) + .collect(); + rendered.insert(player_ent, tiles); + } + + rendered_hand.0 = rendered; + + trace!("render_hands"); + Ok(()) +} diff --git a/src/tui/render/ingame.rs b/src/tui/render/ingame.rs new file mode 100644 index 0000000..bb74910 --- /dev/null +++ b/src/tui/render/ingame.rs @@ -0,0 +1,136 @@ +use bevy::prelude::*; +use bevy_ratatui::RatatuiContext; +use jong::game::hand::DrawnTile; +use jong::game::player::{MainPlayer, Player}; +use jong::tile::Tile; +use ratatui::widgets::{Block, Borders}; + +use crate::tui::render::tile::draw_tile; +use crate::tui::{ + input::mouse::PickRegion, + render::{Hovered, hand, tile::RenderedTile}, +}; + +pub(crate) fn draw_ingame( + mut commands: Commands, + mut tui_ctx: ResMut, + tiles: Query<&Tile>, + hovered_entity: Query>, + main_player: Single, With)>, + rendered_tiles: Populated<&RenderedTile>, + rendered_hand: Res, + drawn_tile: Option>>, +) -> Result { + use ratatui::layout::Flex; + use ratatui::prelude::*; + + tui_ctx.draw(|frame| { + // debug!("{}", frame.area()); + + let term_area = frame.area(); + + // TODO this is gross + let constraints = [Constraint::Fill(3), Constraint::Fill(18)]; + let horizontal_slicer_right = Layout::horizontal(constraints); + let vertical_slicer_bottom = Layout::vertical(constraints); + let horizontal_slicer_left = Layout::horizontal(constraints.iter().rev()); + let vertical_slicer_top = Layout::vertical(constraints.iter().rev()); + let [left_hand, this_hand] = horizontal_slicer_right.areas::<2>(term_area); + let [_, left_hand] = vertical_slicer_bottom.areas::<2>(left_hand); + let [_, mut this_hand] = vertical_slicer_top.areas::<2>(this_hand); + let [cross_hand, right_hand] = horizontal_slicer_left.areas::<2>(term_area); + let [right_hand, _] = vertical_slicer_top.areas::<2>(right_hand); + let [cross_hand, _] = vertical_slicer_bottom.areas::<2>(cross_hand); + + let margin = Margin::new( + (left_hand.width + right_hand.width) / 2, + (cross_hand.height + this_hand.height) / 2, + ); + let pond_area = term_area.inner(margin); + let all_pond = Layout::horizontal([Constraint::Fill(1); 3]); + let cross_pond = + Layout::vertical([Constraint::Fill(1), Constraint::Max(1), Constraint::Fill(1)]); + let [mut left_pond, center, mut right_pond] = all_pond.areas::<3>(pond_area); + let [cross_pond, _compass, this_pond] = cross_pond.areas::<3>(center); + // let shift = left_pond.height - cross_pond.height; + left_pond.height = cross_pond.height; + left_pond.y += cross_pond.height / 2; + right_pond.height = cross_pond.height; + right_pond.y += cross_pond.height / 2; + + let debug_block = Block::new().borders(Borders::ALL); + frame.render_widget(debug_block.clone(), this_hand); + frame.render_widget(debug_block.clone(), left_hand); + frame.render_widget(debug_block.clone(), cross_hand); + frame.render_widget(debug_block.clone(), right_hand); + frame.render_widget(debug_block.clone(), this_pond); + frame.render_widget(debug_block.clone(), left_pond); + frame.render_widget(debug_block.clone(), cross_pond); + frame.render_widget(debug_block.clone(), right_pond); + + // TODO attempt to merge blocks on smol term? + if let Some(hand) = rendered_hand.0.get(&*main_player) { + let hand_area_layout = Layout::horizontal([ + Constraint::Max(hand.len() as u16 * 5), + Constraint::Max(if drawn_tile.is_some() { 7 } else { 0 }), + Constraint::Fill(1), + ]) + .flex(Flex::SpaceBetween); + let this_clamped = this_hand.height.abs_diff(5); + if let Some(_val) = this_hand.height.checked_sub(this_clamped) { + this_hand.height = 4 + } else { + // FIXME show error + panic!("terminal too small!"); + } + this_hand.y += this_clamped + 1; + let [this_hand, mut this_drawn, _this_meld] = hand_area_layout.areas::<3>(this_hand); + + // this_hand + let mut constraints = vec![Constraint::Max(5); hand.len()]; + constraints.push(Constraint::Fill(1)); + let layout = Layout::horizontal(constraints).flex(Flex::Start); + let hand_areas = layout.split(this_hand); + for (tile, mut tile_area) in hand.iter().zip(hand_areas.iter().cloned()) { + // tile_area.height = 4; + let mut widget = rendered_tiles.get(*tile).unwrap().0.clone(); + if hovered_entity.contains(*tile) { + widget = widget.add_modifier(Modifier::BOLD); + if let Some(val) = tile_area.y.checked_sub(1) { + tile_area.y = val + } else { + // FIXME show error + panic!("terminal too small!"); + } + } + commands + .entity(*tile) + .insert(PickRegion { area: tile_area }); + frame.render_widget(widget, tile_area); + } + + // this_drawn + if let Some(tile) = drawn_tile { + // this_drawn.height = 4; + this_drawn.width = 5; + this_drawn.x += 2; + let mut widget = draw_tile(tiles.get(tile.0).unwrap()); + if hovered_entity.contains(tile.0) { + widget = widget.add_modifier(Modifier::BOLD); + if let Some(val) = this_drawn.y.checked_sub(1) { + this_drawn.y = val + } else { + // FIXME show error + panic!("terminal too small!"); + } + } + commands + .entity(tile.0) + .insert(PickRegion { area: this_drawn }); + frame.render_widget(widget, this_drawn); + } + } + })?; + + Ok(()) +} diff --git a/src/tui/render/menu.rs b/src/tui/render/menu.rs new file mode 100644 index 0000000..2e58526 --- /dev/null +++ b/src/tui/render/menu.rs @@ -0,0 +1,96 @@ +use bevy::prelude::*; +use bevy_ratatui::RatatuiContext; +use rand::rng; +use rand::seq::SliceRandom; +use ratatui::layout::Constraint; +use ratatui::layout::Flex; +use ratatui::layout::Layout; +use ratatui::layout::Margin; +use ratatui::widgets::Paragraph; +use ratatui::widgets::{Block, Clear}; + +use jong::tile::Tile; + +use crate::tui::render::WidgetStack; +use crate::tui::render::tile; + +const MAINMENU_OPTIONS: [&str; 2] = [ + " ██╗██████╗ ██╗ ██╗ █████╗ ██╗ ██╗██╗ +██╔╝██╔══██╗╚██╗██║ ██╔══██╗╚██╗ ██╔╝██║ +██║ ██████╔╝ ██║██║ ███████║ ╚████╔╝ ██║ +██║ ██╔═══╝ ██║██║ ██╔══██║ ╚██╔╝ ╚═╝ +╚██╗██║ ██╔╝███████╗██║ ██║ ██║ ██╗ + ╚═╝╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ +", + " ██╗ ██████╗ ██╗ ██╗ ██╗██╗████████╗ ██╗ +██╔╝██╔═══██╗╚██╗██║ ██║██║╚══██╔══╝██╗██╔╝ +██║ ██║ ██║ ██║██║ ██║██║ ██║ ╚═╝██║ +██║ ██║▄▄ ██║ ██║██║ ██║██║ ██║ ██╗██║ +╚██╗╚██████╔╝██╔╝╚██████╔╝██║ ██║ ╚═╝╚██╗ + ╚═╝ ╚══▀▀═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ +", +]; + +pub(crate) fn draw_mainmenu(mut widgets: ResMut) { + let options = MAINMENU_OPTIONS; + let layout = + Layout::vertical(vec![Constraint::Fill(1); options.len()]).flex(Flex::SpaceBetween); + let block = Block::bordered(); + + widgets.0.push(Box::new(move |frame| { + let area = frame + .area() + .centered(Constraint::Max(55), Constraint::Max(19)); + frame.render_widget(Clear, area); + frame.render_widget(block, area); + let areas = layout.split(area.centered(Constraint::Max(45), Constraint::Max(14))); + for (opt, area) in options.into_iter().zip(areas.iter()) { + let para = Paragraph::new(opt); + frame.render_widget(para, *area) + } + })); +} + +#[derive(Resource, Default)] +pub(crate) struct Splash(pub Vec>); + +pub(crate) fn init_splash(mut commands: Commands, tiles: Populated<&Tile>) { + let tiles: Vec<_> = tiles + .iter() + .copied() + .map(|tile| tile::draw_tile(&tile)) + .collect(); + + commands.insert_resource(Splash(tiles)); + trace!("init_splash") +} + +pub(crate) fn render_splash(mut splash: ResMut) { + let mut rng = rng(); + splash.0.shuffle(&mut rng); +} + +pub(crate) fn draw_splash(mut widgets: ResMut, splash: Res) { + let tiles: Vec<_> = splash.0.clone(); + widgets.0.push(Box::new(move |frame| { + let area = frame.area().outer(Margin { + horizontal: 1, + vertical: 1, + }); + let layout = Layout::horizontal(vec![Constraint::Length(5); (area.width / 5) as usize]); + let areas = layout.split(area); + let mut tile_chunks = tiles.chunks(areas.len()).cycle(); + for area in areas.iter() { + let layout = Layout::vertical(vec![Constraint::Length(4); (area.height / 4) as usize]); + let areas = layout.split(*area); + // let tiles: Vec<_> = tile_it + // .by_ref() + // .take((area.height / 4 + 1) as usize) + // .map(|t| tile::draw_tile(&t)) + // .collect(); + for (tile, area) in tile_chunks.next().unwrap().iter().zip(areas.iter()) { + frame.render_widget(tile, *area); + } + } + })); +} diff --git a/src/tui/render/mod.rs b/src/tui/render/mod.rs new file mode 100644 index 0000000..c206fea --- /dev/null +++ b/src/tui/render/mod.rs @@ -0,0 +1,26 @@ +use bevy::prelude::*; +use bevy_ratatui::RatatuiContext; +use ratatui::Frame; + +pub(crate) mod hand; +pub(crate) mod ingame; +pub(crate) mod menu; +pub(crate) mod tile; + +#[derive(Resource, Default)] +pub(crate) struct WidgetStack(pub(crate) Vec>); + +#[derive(Component)] +pub(crate) struct Hovered; + +pub(crate) fn draw_system( + mut tui_ctx: ResMut, + mut widgetstack: ResMut, +) -> Result { + tui_ctx.draw(|frame| { + for widget in widgetstack.0.drain(..) { + widget(frame) + } + })?; + Ok(()) +} diff --git a/src/tui/render/tile.rs b/src/tui/render/tile.rs new file mode 100644 index 0000000..95bf282 --- /dev/null +++ b/src/tui/render/tile.rs @@ -0,0 +1,34 @@ +use bevy::prelude::*; +use ratatui::widgets::Paragraph; + +use jong::tile::Tile; + +#[derive(Component)] +pub(crate) struct RenderedTile(pub(crate) Paragraph<'static>); + +pub(crate) fn draw_tile(tile: &Tile) -> Paragraph<'static> { + + + let block = ratatui::widgets::Block::bordered(); + + Paragraph::new(match &tile.suit { + jong::tile::Suit::Pin(rank) => format!("{}\np", rank.0), + jong::tile::Suit::Sou(rank) => format!("{}\ns", rank.0), + jong::tile::Suit::Man(rank) => format!("{}\nm", rank.0), + jong::tile::Suit::Wind(wind) => (match wind { + jong::tile::Wind::Ton => "e\nw", + jong::tile::Wind::Nan => "s\nw", + jong::tile::Wind::Shaa => "w\nw", + jong::tile::Wind::Pei => "n\nw", + }) + .into(), + jong::tile::Suit::Dragon(dragon) => (match dragon { + jong::tile::Dragon::Haku => "w\nd", + jong::tile::Dragon::Hatsu => "g\nd", + jong::tile::Dragon::Chun => "r\nd", + }) + .into(), + }) + .block(block) + .centered() +}