1#[cfg(not(target_arch = "wasm32"))]
2use std::time::Instant;
3
4use bracket_lib::prelude::*;
5
6use legion::*;
7
8use crate::{
9 components::{GameCell, Unit},
10 types::{CtrlGroups, Mode, Race, UnitKind},
11};
12
13const WHITE: (u8, u8, u8) = (255, 255, 255);
14const DARK_GRAY: (u8, u8, u8) = (100, 100, 100);
15const BROWN: (u8, u8, u8) = (170, 30, 0);
16const GREEN: (u8, u8, u8) = (0, 170, 0);
17const DARK_GREEN: (u8, u8, u8) = (0, 120, 0);
18
19#[derive(Clone, Debug)]
20pub enum CurrentState {
21 Menu,
22 Playing,
23 Quitting,
24}
25
26pub struct State {
27 curr_state: CurrentState,
28 world: World,
29 schedule: Schedule,
30 window_size: (u32, u32),
31 tic: u8,
32 dt: f32,
33 #[cfg(not(target_arch = "wasm32"))]
34 instant: Instant,
35 offset: (i32, i32),
36 mouse: Point,
37 mouse_click: Option<(usize, bool)>,
38 mouse_pressed: (usize, bool, bool),
39 mode: Mode,
40 cursor: String,
41 selection: Rect,
42 selected: Vec<Entity>,
43 ctrl_groups: CtrlGroups,
44}
45
46impl State {
47 pub fn new(w: u32, h: u32) -> Self {
48 let mut world = World::default();
49
50 let mut units = Vec::new();
51 for x in 0..20 {
52 units.push((
53 GameCell::new(10 - (x & 1), x + 5, 'V', RGB::named(GREEN)),
54 Unit::new(Race::Bionic, UnitKind::Blademaster, 30)
55 .with_damage(5)
56 .with_speed(14.5),
57 ));
58 units.push((
59 GameCell::new(7 - (x & 1), x + 5, 'Y', RGB::named(DARK_GREEN)),
60 Unit::new(Race::Bionic, UnitKind::Strider, 40)
61 .with_damage(5)
62 .with_range(10, 13),
63 ));
64 }
65 for _ in 0..5 {
66 for y in 0..20 {
67 units.push((
68 GameCell::new(45, 5 + y, '*', RGB::named(BROWN)),
69 Unit::new(Race::Bug, UnitKind::FleshSpider, 15),
70 ));
71 }
72 }
73 world.extend(units);
74
75 let bump_units = SystemBuilder::new("bump_units")
76 .with_query(<(Read<GameCell>,)>::query().filter(component::<Unit>()))
77 .with_query(<(Read<GameCell>,)>::query())
78 .write_component::<GameCell>()
79 .build(|_, world, _, (query, inner_query)| {
80 let mut bumped = Vec::new();
81 for chunk in query.iter_chunks(world) {
82 for (e, (cell,)) in chunk.into_iter_entities() {
83 for inner_chunk in inner_query.iter_chunks(world) {
84 for (e2, (cell2,)) in inner_chunk.into_iter_entities() {
85 if !cell.is_holding() && e != e2 && cell.point() == cell2.point() {
86 bumped.push(e);
87 break;
88 }
89 }
90 }
91 }
92 }
93 for e in bumped.iter() {
94 if let Ok(cell) = world.entry_mut(*e).unwrap().get_component_mut::<GameCell>() {
95 cell.bump();
96 }
97 }
98 });
99
100 let attack_units = SystemBuilder::new("attack_units")
101 .with_query(<(Read<GameCell>, Read<Unit>)>::query())
102 .with_query(<(Read<GameCell>, Read<Unit>)>::query())
103 .with_query(<(Read<GameCell>, Read<Unit>)>::query())
104 .write_component::<GameCell>()
105 .write_component::<Unit>()
106 .build(|_, world, _, (query, attack_query, moving_query)| {
107 let mut attacked_units = Vec::new();
108 let mut moving_units = Vec::new();
109 for chunk in query.iter_chunks(world) {
110 for (e, (cell, unit)) in chunk.into_iter_entities() {
111 let mut attacked = false;
112 for attack_chunk in attack_query.iter_chunks(world) {
113 for (e2, (cell2, unit2)) in attack_chunk.into_iter_entities() {
114 if e != e2
115 && unit.race() != unit2.race()
116 && cell.range_rect(unit.range()).point_in_rect(cell2.point())
117 {
118 if let Some(damage) = unit.attack() {
119 attacked_units.push((e, e2, damage));
120 }
121 attacked = true;
122 break;
123 }
124 }
125 }
126 if !attacked {
127 for moving_chunk in moving_query.iter_chunks(world) {
128 for (e2, (cell2, unit2)) in moving_chunk.into_iter_entities() {
129 if e != e2
130 && !cell.is_holding()
131 && unit.race() != unit2.race()
132 && cell
133 .range_rect(unit.follow_dist())
134 .point_in_rect(cell2.point())
135 {
136 moving_units.push((e, cell2.point()));
137 break;
138 }
139 }
140 }
141 }
142 }
143 }
144 for (e, e2, dmg) in attacked_units.iter() {
145 if let Ok(cell) = world.entry_mut(*e).unwrap().get_component_mut::<GameCell>() {
146 cell.stop_moving();
147 }
148 if let Ok(unit) = world.entry_mut(*e).unwrap().get_component_mut::<Unit>() {
149 unit.reset_tic();
150 }
151 if let Ok(cell2) = world
152 .entry_mut(*e2)
153 .unwrap()
154 .get_component_mut::<GameCell>()
155 {
156 cell2.set_harmed();
157 }
158 if let Ok(unit2) = world.entry_mut(*e2).unwrap().get_component_mut::<Unit>() {
159 unit2.harm(*dmg);
160 }
161 }
162 for (e, pt) in moving_units.iter() {
163 if let Ok(cell) = world.entry_mut(*e).unwrap().get_component_mut::<GameCell>() {
164 cell.move_towards(*pt);
165 }
166 }
167 });
168
169 let clear_units = SystemBuilder::new("clear_units")
170 .with_query(<(Read<Unit>,)>::query().filter(maybe_changed::<Unit>()))
171 .write_component::<Unit>()
172 .build(|commands, world, _, query| {
173 let mut deleted = Vec::new();
174 for chunk in query.iter_chunks(world) {
175 for (e, (unit,)) in chunk.into_iter_entities() {
176 if unit.hp() <= 0 {
177 deleted.push(e);
178 }
179 }
180 }
181 for e in deleted.iter() {
182 commands.remove(*e);
183 }
184 });
185
186 let schedule = Schedule::builder()
187 .add_system(bump_units)
188 .add_system(attack_units)
189 .add_system(clear_units)
190 .flush()
191 .build();
192
193 Self {
194 curr_state: CurrentState::Menu,
195 world,
196 schedule,
197 window_size: (w, h),
198 dt: 0.016,
199 #[cfg(not(target_arch = "wasm32"))]
200 instant: Instant::now(),
201 tic: 0,
202 offset: (0, 0),
203 mouse: Point::new(0, 0),
204 mouse_click: None,
205 mouse_pressed: (0, false, false),
206 mode: Mode::Select,
207 cursor: String::from("<"),
208 selection: Rect::default(),
209 selected: Vec::new(),
210 ctrl_groups: CtrlGroups::new(),
211 }
212 }
213
214 fn menu_state(&mut self, ctx: &mut BTerm) {
215 ctx.print_centered(self.window_size.1 as i32 / 2 - 1, "TextRTS");
216 ctx.print_centered(
217 self.window_size.1 as i32 / 2 + 1,
218 "Press the spacebar to start",
219 );
220
221 if let Some(VirtualKeyCode::Space) = ctx.key {
222 self.curr_state = CurrentState::Playing;
223 }
224 }
225
226 fn play_state(&mut self, ctx: &mut BTerm) {
227 let mut resources = Resources::default();
228 self.schedule.execute(&mut self.world, &mut resources);
229
230 self.print_grid(ctx);
231
232 ctx.print_color(
233 self.mouse.x,
234 self.mouse.y,
235 RGB::named((0, 155 + self.tic, 0)),
236 RGB::new(),
237 &self.cursor,
238 );
239
240 self.render_cells(ctx);
241
242 self.print_mode(ctx);
243
244 self.mouse_input();
245
246 self.key_input(ctx);
247
248 self.draw_highlight_box(ctx);
249 }
250
251 fn mode(&self) -> Mode {
252 self.mode
253 }
254 fn set_mode(&mut self, mode: Mode) {
255 self.mode = mode;
256 }
257
258 fn mouse_input(&mut self) {
259 if self.mouse.x <= 0 {
260 self.offset.0 += 1;
261 } else if self.mouse.x >= self.window_size.0 as i32 - 1 {
262 self.offset.0 -= 1;
263 }
264 if self.mouse.y <= 0 {
265 self.offset.1 += 1;
266 } else if self.mouse.y >= self.window_size.1 as i32 - 1 {
267 self.offset.1 -= 1;
268 }
269
270 match self.mouse_click {
271 Some((0, false)) => match self.mode() {
272 Mode::Select => self.select_cells(),
273 Mode::Move | Mode::Attack => {
274 self.move_cells();
275 self.set_mode(Mode::Select);
276 }
277 Mode::Ctrl => self.select_same(),
278 _ => (),
279 },
280 Some((1, false)) => {
281 self.move_cells();
282 self.set_mode(Mode::Select);
283 }
284 _ => (),
285 }
286 }
287
288 fn key_num(key: VirtualKeyCode) -> Option<usize> {
289 match key {
290 VirtualKeyCode::Key0 => Some(0),
291 VirtualKeyCode::Key1 => Some(1),
292 VirtualKeyCode::Key2 => Some(2),
293 VirtualKeyCode::Key3 => Some(3),
294 VirtualKeyCode::Key4 => Some(4),
295 VirtualKeyCode::Key5 => Some(5),
296 VirtualKeyCode::Key6 => Some(6),
297 VirtualKeyCode::Key7 => Some(7),
298 VirtualKeyCode::Key8 => Some(8),
299 VirtualKeyCode::Key9 => Some(9),
300 _ => None,
301 }
302 }
303
304 fn key_input(&mut self, ctx: &mut BTerm) {
305 if let Some(key) = ctx.key {
306 match self.mode {
307 Mode::Ctrl => {
308 if let Some(n) = State::key_num(key) {
309 self.ctrl_groups.bind(n, self.selected.clone());
310 }
311 self.set_mode(Mode::Select);
312 }
313 Mode::Add => {
314 if let Some(n) = State::key_num(key) {
315 self.ctrl_groups.add(n, &mut self.selected.clone());
316 }
317 self.set_mode(Mode::Select);
318 }
319 _ => match key {
320 VirtualKeyCode::M => self.set_mode(Mode::Move),
321 VirtualKeyCode::A => self.set_mode(Mode::Attack),
322 VirtualKeyCode::B => self.set_mode(Mode::Build),
323 VirtualKeyCode::S => self.stop_cells(),
324 VirtualKeyCode::H => self.hold_cells(),
325 VirtualKeyCode::F => self.focus_cell(),
326
327 VirtualKeyCode::LControl | VirtualKeyCode::RControl => {
328 self.set_mode(Mode::Ctrl)
329 }
330 VirtualKeyCode::LShift | VirtualKeyCode::RShift => self.set_mode(Mode::Add),
331
332 VirtualKeyCode::Escape => self.set_mode(Mode::Select),
333 VirtualKeyCode::Up => self.offset.1 += 1,
334 VirtualKeyCode::Down => self.offset.1 -= 1,
335 VirtualKeyCode::Left => self.offset.0 += 1,
336 VirtualKeyCode::Right => self.offset.0 -= 1,
337 VirtualKeyCode::End => self.curr_state = CurrentState::Quitting,
338 _ => {
339 if let Some(n) = State::key_num(key) {
340 if let Some(group) = self.ctrl_groups.group(n) {
341 self.selected = group.clone();
342 self.load_ctrl_group();
343 }
344 }
345 }
346 },
347 }
348 }
349 }
350
351 fn draw_highlight_box(&mut self, ctx: &mut BTerm) {
352 if let Mode::Select = self.mode {
353 self.selection.x2 = self.mouse.x;
354 self.selection.y2 = self.mouse.y;
355 if self.mouse_pressed.0 == 0 && self.mouse_pressed.1 {
356 let x = if self.selection.x1 <= self.selection.x2 {
357 self.selection.x1
358 } else {
359 self.selection.x2
360 };
361 let y = if self.selection.y1 <= self.selection.y2 {
362 self.selection.y1
363 } else {
364 self.selection.y2
365 };
366 ctx.draw_hollow_box(
367 x,
368 y,
369 self.selection.width(),
370 self.selection.height(),
371 RGB::named(GREEN),
372 RGB::new(),
373 );
374 } else {
375 self.selection.x1 = self.mouse.x;
376 self.selection.y1 = self.mouse.y;
377 }
378 }
379 }
380
381 fn print_grid(&mut self, ctx: &mut BTerm) {
382 for x in 0..self.window_size.0 {
383 for y in 0..self.window_size.1 - 1 {
384 ctx.print_color(x as i32, y as i32, RGB::named(DARK_GRAY), RGB::new(), ".")
385 }
386 }
387 }
388
389 fn print_mode(&mut self, ctx: &mut BTerm) {
390 let mut w = 0;
391 let mut color = RGB::new();
392 let mut s = "";
393 match self.mode() {
394 Mode::Move => {
395 w = 5;
396 color = RGB::from_u8(0, 175, 0);
397 s = "Move";
398 }
399 Mode::Attack => {
400 w = 7;
401 color = RGB::from_u8(175, 0, 0);
402 s = "Attack";
403 }
404 Mode::Build => {
405 w = 6;
406 color = RGB::from_u8(0, 0, 175);
407 s = "Build";
408 }
409 Mode::Ctrl => {
410 w = 5;
411 color = RGB::from_u8(75, 75, 75);
412 s = "Ctrl"
413 }
414 Mode::Add => {
415 w = 4;
416 color = RGB::from_u8(75, 75, 75);
417 s = "Add"
418 }
419 _ => (),
420 }
421 ctx.draw_box(0, 0, w, 2, color, color);
422 ctx.print_color(1, 1, RGB::named(WHITE), color, s)
423 }
424
425 fn render_cells(&mut self, ctx: &mut BTerm) {
426 let mut query = <(Write<GameCell>, Write<Unit>)>::query();
427
428 for (cell, unit) in query.iter_mut(&mut self.world) {
429 if Rect::with_exact(
430 -self.offset.0,
431 -self.offset.1,
432 self.window_size.0 as i32 - self.offset.0,
433 self.window_size.1 as i32 - self.offset.1,
434 )
435 .point_in_rect(cell.point())
436 {
437 ctx.print_color(
438 cell.x() + self.offset.0,
439 cell.y() + self.offset.1,
440 if self.mouse.x - self.offset.0 == cell.x()
441 && self.mouse.y - self.offset.1 == cell.y()
442 {
443 cell.color_bright()
444 } else {
445 cell.color()
446 },
447 cell.bg_color(),
448 &cell.symbol().to_string(),
449 );
450 }
451
452 cell.update(self.dt, unit.speed());
453 unit.tic(self.dt);
454 }
455 }
456
457 fn load_ctrl_group(&mut self) {
458 let mut query = <(Write<GameCell>,)>::query();
459
460 for chunk in query.iter_chunks_mut(&mut self.world) {
461 for (e, (cell,)) in chunk.into_iter_entities() {
462 if self.selected.contains(&e) {
463 cell.select();
464 } else {
465 cell.deselect();
466 }
467 }
468 }
469 }
470
471 fn select_cells(&mut self) {
472 let mut query = <(Write<GameCell>,)>::query();
473
474 self.selected = Vec::new();
475
476 if self.selection.width() == 0 || self.selection.height() == 0 {
477 for chunk in query.iter_chunks_mut(&mut self.world) {
478 for (e, (cell,)) in chunk.into_iter_entities() {
479 if self.mouse.x == cell.x() + self.offset.0
480 && self.mouse.y == cell.y() + self.offset.1
481 {
482 cell.select();
483 self.selected.push(e);
484 break;
485 } else {
486 cell.deselect();
487 }
488 }
489 }
490 } else {
491 for chunk in query.iter_chunks_mut(&mut self.world) {
492 for (e, (cell,)) in chunk.into_iter_entities() {
493 let x = if self.selection.x1 <= self.selection.x2 {
494 self.selection.x1
495 } else {
496 self.selection.x2
497 };
498 let y = if self.selection.y1 <= self.selection.y2 {
499 self.selection.y1
500 } else {
501 self.selection.y2
502 };
503 if Rect::with_size(
504 x,
505 y,
506 self.selection.width() + 1,
507 self.selection.height() + 1,
508 )
509 .point_in_rect(Point::new(
510 cell.x() + self.offset.0,
511 cell.y() + self.offset.1,
512 )) {
513 cell.select();
514 self.selected.push(e);
515 } else {
516 cell.deselect();
517 }
518 }
519 }
520 }
521 }
522
523 fn select_same(&mut self) {
524 let mut query = <(Write<GameCell>, Read<Unit>)>::query();
525
526 self.selected = Vec::new();
527
528 let mut kind = None;
529
530 if self.selection.width() == 0 || self.selection.height() == 0 {
531 for (cell, unit) in query.iter_mut(&mut self.world) {
532 if self.mouse.x == cell.x() + self.offset.0
533 && self.mouse.y == cell.y() + self.offset.1
534 {
535 kind = Some(unit.kind());
536 break;
537 }
538 }
539 }
540 if let Some(kind) = kind {
541 for chunk in query.iter_chunks_mut(&mut self.world) {
542 for (e, (cell, unit)) in chunk.into_iter_entities() {
543 if kind == unit.kind()
544 && cell.x() + self.offset.0 > 0
545 && cell.y() + self.offset.1 > 0
546 && cell.x() + self.offset.0 < self.window_size.0 as i32
547 && cell.y() + self.offset.1 < self.window_size.1 as i32
548 {
549 cell.select();
550 self.selected.push(e);
551 } else {
552 cell.deselect();
553 }
554 }
555 }
556 }
557
558 self.mode = Mode::Select;
559 }
560
561 fn move_cells(&mut self) {
562 let mut query = <(Write<GameCell>, Write<Unit>)>::query();
563
564 for (cell, unit) in query.iter_mut(&mut self.world) {
565 if cell.selected() {
566 cell.move_pos(Point::new(
567 self.mouse.x - self.offset.0,
568 self.mouse.y - self.offset.1,
569 ));
570 unit.reset_tic();
571 }
572 }
573 }
574
575 fn stop_cells(&mut self) {
576 let mut query = <(Write<GameCell>, Write<Unit>)>::query();
577
578 for (cell, _) in query.iter_mut(&mut self.world) {
579 if cell.selected() {
580 cell.stop_moving();
581 }
582 }
583 }
584
585 fn hold_cells(&mut self) {
586 let mut query = <(Write<GameCell>, Write<Unit>)>::query();
587
588 for (cell, _) in query.iter_mut(&mut self.world) {
589 if cell.selected() {
590 cell.stop_moving();
591 cell.hold();
592 }
593 }
594 }
595
596 fn focus_cell(&mut self) {
597 let mut query = <(Write<GameCell>, Write<Unit>)>::query();
598
599 for (cell, _) in query.iter_mut(&mut self.world) {
600 if cell.selected() {
601 self.offset = (
602 -cell.x() + self.window_size.0 as i32 / 2,
603 -cell.y() + self.window_size.1 as i32 / 2,
604 );
605 }
606 }
607 }
608
609 fn quit_state(&mut self, ctx: &mut BTerm) {
610 ctx.print(5, 5, "Are you sure you want to quit? (y/n)");
611
612 if let Some(VirtualKeyCode::Y) = ctx.key {
613 ctx.quit();
614 } else if let Some(VirtualKeyCode::N) = ctx.key {
615 self.curr_state = CurrentState::Playing;
616 }
617 }
618
619 #[cfg(target_arch = "wasm32")]
620 fn update_dt(&self) {}
621 #[cfg(not(target_arch = "wasm32"))]
622 fn update_dt(&mut self) {
623 self.dt = Instant::now().duration_since(self.instant).as_secs_f32();
624 self.instant = Instant::now();
625 }
626
627 #[cfg(target_arch = "wasm32")]
628 fn get_input(&mut self) {
629 self.mouse_pressed.2 = false;
630
631 let mut input = INPUT.lock();
632
633 input.for_each_message(|event| match event {
634 BEvent::MouseButtonUp { button } => {
635 self.mouse_pressed = (button, false, self.mouse_pressed.1)
636 }
637 BEvent::MouseButtonDown { button } => {
638 self.mouse_pressed = (button, true, self.mouse_pressed.1)
639 }
640 _ => (),
641 });
642
643 if !self.mouse_pressed.1 && self.mouse_pressed.2 {
644 self.mouse_click = Some((self.mouse_pressed.0, false))
645 }
646 }
647 #[cfg(not(target_arch = "wasm32"))]
648 fn get_input(&mut self) {
649 let mut input = INPUT.lock();
650
651 input.for_each_message(|event| match event {
652 BEvent::MouseClick { button, pressed } => self.mouse_click = Some((button, pressed)),
653 BEvent::MouseButtonUp { button } => self.mouse_pressed = (button, false, false),
654 BEvent::MouseButtonDown { button } => self.mouse_pressed = (button, true, false),
655 _ => (),
656 });
657 }
658}
659
660impl GameState for State {
661 fn tick(&mut self, ctx: &mut BTerm) {
662 self.update_dt();
663
664 ctx.cls();
665
666 self.get_input();
667
668 self.tic += 4;
669 if self.tic > 99 {
670 self.tic = 0;
671 }
672
673 self.mouse = ctx.mouse_point();
674
675 match self.curr_state {
676 CurrentState::Menu => self.menu_state(ctx),
677 CurrentState::Playing => self.play_state(ctx),
678 CurrentState::Quitting => self.quit_state(ctx),
679 }
680
681 self.mouse_click = None;
682 }
683}