1use std::{
2 io::{stdout, Write},
3 ops::Deref,
4 process,
5};
6
7use legion::prelude::*;
8
9use crossterm::{
10 event::{self, Event, KeyCode},
11 execute,
12 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
13};
14
15use tui::{
16 backend::CrosstermBackend,
17 layout::{Constraint, Corner, Direction, Layout},
18 style::Color,
19 widgets::{canvas::Canvas, Block, Borders, List, Widget},
20 Terminal,
21};
22
23use crate::{CellAccess, CellKind, CellVisibility, GameCell, GameEvents, Inventory, Player};
24
25pub struct TuiSystem;
26
27impl TuiSystem {
28 pub fn run(world: &mut World) {
29 let read_query = <(Read<GameCell>,)>::query();
30 let write_query = <(legion::query::Write<GameCell>,)>::query();
31
32 enable_raw_mode().unwrap();
33
34 let mut stdout = stdout();
35 execute!(stdout, EnterAlternateScreen).unwrap();
36
37 let mut terminal = Terminal::new(CrosstermBackend::new(stdout)).unwrap();
38 terminal.hide_cursor().unwrap();
39 terminal.clear().unwrap();
40
41 let term_width = terminal.size().unwrap().width;
42 let term_height = terminal.size().unwrap().height;
43 let canvas_width = term_width - 25;
44 let canvas_height = term_height - 8;
45
46 let player = Player::new(
47 (canvas_width as f64 / 2.0).round(),
48 (canvas_height as f64 / 2.0).round(),
49 );
50
51 let mut game_events = GameEvents::new();
52
53 let mut inventory = Inventory::new();
54
55 loop {
56 if let Event::Key(key) = event::read().unwrap() {
57 match key.code {
58 KeyCode::Up => {
59 let mut collided = false;
60 for (gamecell,) in read_query.iter_immutable(world) {
61 if gamecell.access() == CellAccess::Impassable
62 && (player.x() - gamecell.x() as f64).abs() < 1.0
63 && (player.y() - (gamecell.y() - 1) as f64).abs() < 1.0
64 {
65 game_events.post_event(
66 format!(
67 "You ran into the {}.{space:>width$}",
68 gamecell.name(),
69 space = " ",
70 width = canvas_width as usize / 2,
71 ),
72 Color::Blue,
73 );
74 collided = true;
75 break;
76 }
77 }
78 if !collided {
79 write_query.par_for_each(world, {
80 |(mut gamecell,)| {
81 if gamecell.inside(
82 player.x() as u16 - 5,
83 player.y() as u16 - 5,
84 player.x() as u16 + 5,
85 player.y() as u16 + 5,
86 ) {
87 gamecell.set_visible(CellVisibility::Visible);
88 } else if gamecell.visible() == CellVisibility::Visible {
89 gamecell.set_visible(CellVisibility::Dark);
90 }
91 gamecell.move_up();
92 }
93 });
94 }
95 }
96 KeyCode::Down => {
97 let mut collided = false;
98 for (gamecell,) in read_query.iter_immutable(world) {
99 if gamecell.access() == CellAccess::Impassable
100 && (player.x() - gamecell.x() as f64).abs() < 1.0
101 && (player.y() - (gamecell.y() + 1) as f64).abs() < 1.0
102 {
103 game_events.post_event(
104 format!(
105 "You ran into the {}.{space:>width$}",
106 gamecell.name(),
107 space = " ",
108 width = canvas_width as usize / 2,
109 ),
110 Color::Blue,
111 );
112 collided = true;
113 break;
114 }
115 }
116 if !collided {
117 write_query.par_for_each(world, {
118 |(mut gamecell,)| {
119 if gamecell.inside(
120 player.x() as u16 - 5,
121 player.y() as u16 - 5,
122 player.x() as u16 + 5,
123 player.y() as u16 + 5,
124 ) {
125 gamecell.set_visible(CellVisibility::Visible);
126 } else if gamecell.visible() == CellVisibility::Visible {
127 gamecell.set_visible(CellVisibility::Dark);
128 }
129 gamecell.move_down();
130 }
131 });
132 }
133 }
134 KeyCode::Left => {
135 let mut collided = false;
136 for (gamecell,) in read_query.iter_immutable(world) {
137 if gamecell.access() == CellAccess::Impassable
138 && (player.x() - (gamecell.x() + 1) as f64).abs() < 1.0
139 && (player.y() - gamecell.y() as f64).abs() < 1.0
140 {
141 game_events.post_event(
142 format!(
143 "You ran into the {}.{space:>width$}",
144 gamecell.name(),
145 space = " ",
146 width = canvas_width as usize / 2,
147 ),
148 Color::Blue,
149 );
150 collided = true;
151 break;
152 }
153 }
154 if !collided {
155 write_query.par_for_each(world, {
156 |(mut gamecell,)| {
157 if gamecell.inside(
158 player.x() as u16 - 5,
159 player.y() as u16 - 5,
160 player.x() as u16 + 5,
161 player.y() as u16 + 5,
162 ) {
163 gamecell.set_visible(CellVisibility::Visible);
164 } else if gamecell.visible() == CellVisibility::Visible {
165 gamecell.set_visible(CellVisibility::Dark);
166 }
167 gamecell.move_right();
168 }
169 });
170 }
171 }
172 KeyCode::Right => {
173 let mut collided = false;
174 for (gamecell,) in read_query.iter_immutable(world) {
175 if gamecell.access() == CellAccess::Impassable
176 && (player.x() - (gamecell.x() - 1) as f64).abs() < 1.0
177 && (player.y() - gamecell.y() as f64).abs() < 1.0
178 {
179 game_events.post_event(
180 format!(
181 "You ran into the {}.{space:>width$}",
182 gamecell.name(),
183 space = " ",
184 width = canvas_width as usize / 2,
185 ),
186 Color::Blue,
187 );
188 collided = true;
189 break;
190 }
191 }
192 if !collided {
193 write_query.par_for_each(world, {
194 |(mut gamecell,)| {
195 if gamecell.inside(
196 player.x() as u16 - 5,
197 player.y() as u16 - 5,
198 player.x() as u16 + 5,
199 player.y() as u16 + 5,
200 ) {
201 gamecell.set_visible(CellVisibility::Visible);
202 } else if gamecell.visible() == CellVisibility::Visible {
203 gamecell.set_visible(CellVisibility::Dark);
204 }
205 gamecell.move_left();
206 }
207 });
208 }
209 }
210 KeyCode::Char('q') => {
211 terminal.clear().unwrap();
212 terminal.show_cursor().unwrap();
213 disable_raw_mode().unwrap();
214 execute!(terminal.backend_mut(), LeaveAlternateScreen).unwrap();
215 process::exit(1);
216 }
217 _ => (),
218 }
219 }
220
221 let read_query = <(Read<GameCell>,)>::query();
222
223 let mut taken = None;
224 for (entity, (gamecell,)) in read_query.iter_entities_immutable(world) {
225 if gamecell.access() == CellAccess::Takeable
226 && gamecell.inside(1, 1, term_width, term_height)
227 && (player.x() - gamecell.x() as f64).abs() < 1.0
228 && (player.y() - gamecell.y() as f64).abs() < 1.0
229 {
230 game_events.post_event(
231 format!(
232 "You now have the {}.{space:>width$}",
233 gamecell.name(),
234 space = " ",
235 width = canvas_width as usize / 2,
236 ),
237 Color::Green,
238 );
239 inventory.take(gamecell.deref().clone());
240 taken = Some(entity);
241 break;
242 }
243 }
244 if let Some(entity) = taken {
245 world.delete(entity);
246 }
247
248 terminal
249 .draw(|mut f| {
250 let chunks = Layout::default()
251 .margin(0)
252 .direction(Direction::Vertical)
253 .constraints(
254 [
255 Constraint::Length(canvas_height + 1),
256 Constraint::Length(term_height - canvas_height - 2),
257 ]
258 .as_ref(),
259 )
260 .split(f.size());
261 let top_chunks = Layout::default()
262 .margin(0)
263 .direction(Direction::Horizontal)
264 .constraints(
265 [
266 Constraint::Length(canvas_width + 1),
267 Constraint::Length(term_width - canvas_width - 2),
268 ]
269 .as_ref(),
270 )
271 .split(chunks[0]);
272 let bottom_chunks = Layout::default()
273 .margin(0)
274 .direction(Direction::Horizontal)
275 .constraints(
276 [Constraint::Percentage(70), Constraint::Percentage(30)].as_ref(),
277 )
278 .split(chunks[1]);
279 Canvas::default()
280 .block(Block::default().borders(Borders::ALL).title("Game"))
281 .paint(|ctx| {
282 for (gamecell,) in read_query.iter_immutable(world) {
283 if gamecell.visible() != CellVisibility::Unvisited
284 && gamecell.inside(1, 1, term_width, term_height)
285 {
286 let symbol = match gamecell.kind() {
287 CellKind::SoftArmor => "(",
288 CellKind::HardArmor => "[",
289 CellKind::BluntWeapon => "\\",
290 CellKind::EdgedWeapon => "|",
291 CellKind::PointedWeapon => "/",
292 CellKind::RangedWeapon => "}",
293 CellKind::ClosedDoor => "+",
294 CellKind::OpenedDoor => "'",
295 CellKind::Wall => "#",
296 CellKind::Floor => ".",
297 };
298 if gamecell.visible() == CellVisibility::Visible {
299 ctx.print(
300 gamecell.x() as f64,
301 gamecell.y() as f64,
302 symbol,
303 gamecell.color(),
304 );
305 } else {
306 ctx.print(
307 gamecell.x() as f64,
308 gamecell.y() as f64,
309 symbol,
310 Color::DarkGray,
311 );
312 }
313 }
314 }
315 ctx.print(player.x(), player.y(), "@", Color::Rgb(0, 255, 0));
316 })
317 .x_bounds([2.0, canvas_width as f64])
318 .y_bounds([2.0, canvas_height as f64])
319 .render(&mut f, top_chunks[0]);
320 List::new(inventory.list().into_iter())
321 .block(Block::default().borders(Borders::ALL).title("Inventory"))
322 .start_corner(Corner::TopLeft)
323 .render(&mut f, top_chunks[1]);
324 List::new(game_events.events().clone().into_iter())
325 .block(Block::default().borders(Borders::ALL).title("Events"))
326 .start_corner(Corner::TopLeft)
327 .render(&mut f, bottom_chunks[0]);
328 List::new(player.list().into_iter())
329 .block(Block::default().borders(Borders::ALL).title("Player"))
330 .start_corner(Corner::TopLeft)
331 .render(&mut f, bottom_chunks[1]);
332 })
333 .unwrap();
334 }
335 }
336}