From 7ef25034fd8d4d5da22e8d9402a4302c962e3edc Mon Sep 17 00:00:00 2001 From: Paul Brinkmeier Date: Sat, 29 Nov 2025 14:56:53 +0100 Subject: [PATCH] Impl basic loop --- src/game.rs | 149 ++++++++++++++++++++++++++++++++++++++++++++++++ src/geometry.rs | 94 +++++++++++++++++++++--------- src/main.rs | 50 ++++++++-------- 3 files changed, 243 insertions(+), 50 deletions(-) create mode 100644 src/game.rs diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 0000000..0302dd4 --- /dev/null +++ b/src/game.rs @@ -0,0 +1,149 @@ +use crossterm::event::{Event, KeyCode}; +use ratatui::{ + Frame, buffer::{Buffer, Cell}, layout::Rect, style::Color, widgets::{Block, Widget} +}; + +use crate::geometry::{Grid, P2, V2}; + +pub struct GameModel { + player_pos: P2, + room: Room, +} + +struct Room { + size: V2, +} + +impl GameModel { + pub fn new() -> Self { + Self { + player_pos: P2::new(0, 0), + room: Room { + size: V2::new(20, 10), + }, + } + } + + pub fn update(self, event: Event) -> Option { + match event { + Event::Key(key_event) => match key_event.code { + KeyCode::Char('q') => return None, + KeyCode::Char('j') => { + return Some(Self { + player_pos: self.player_pos + V2::new(0, 1), + ..self + }) + }, + KeyCode::Char('k') => { + return Some(Self { + player_pos: self.player_pos + V2::new(0, -1), + ..self + }) + }, + KeyCode::Char('h') => { + return Some(Self { + player_pos: self.player_pos + V2::new(-1, 0), + ..self + }) + }, + KeyCode::Char('l') => { + return Some(Self { + player_pos: self.player_pos + V2::new(1, 0), + ..self + }) + }, + _ => (), + }, + _ => (), + } + + Some(self) + } + + pub fn render(&self, frame: &mut Frame) { + let camera_block = Block::bordered().title("camera"); + let camera_area = camera_block.inner(frame.area()); + let camera = CameraWidget::new(self); + + frame.render_widget(camera_block, frame.area()); + frame.render_widget(camera, camera_area); + } + + fn render_tiles(&self) -> Grid { + let mut tiles = Grid::from_fn(self.room.size.x, self.room.size.y, |_x, _y| Tile::Floor); + + tiles + .get_mut(P2::new(1, 1)) + .map(|tile| *tile = Tile::Ladder); + + tiles + .get_mut(P2::new(5, 2)) + .map(|tile| *tile = Tile::Amphora); + + tiles + .get_mut(P2::new(8, 5)) + .map(|tile| *tile = Tile::Frog); + + tiles + .get_mut(self.player_pos) + .map(|tile| *tile = Tile::Player); + + + tiles + } +} + +enum Tile { + Floor, + Ladder, + Player, + Amphora, + Frog, +} + +impl Tile { + fn render(&self, cell: &mut Cell) { + match self { + Tile::Floor => { + cell.set_symbol(" "); + } + Tile::Ladder => { + cell.set_symbol("Ħ"); + } + Tile::Player => { + cell.set_symbol("▲̶͈̊"); + } + Tile::Amphora => { + cell.set_symbol("⚱").set_fg(Color::LightRed); + } + Tile::Frog => { + cell.set_symbol("ä̃").set_fg(Color::Green); + } + } + } +} + +struct CameraWidget<'a> { + model: &'a GameModel, +} +impl<'a> CameraWidget<'a> { + fn new(model: &'a GameModel) -> Self { + Self { model } + } +} + +impl<'a> Widget for CameraWidget<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let tiles = self.model.render_tiles(); + for y in 0..area.height { + for x in 0..area.width { + let cell = &mut buf[(area.left() + x, area.top() + y)]; + if let Some(tile) = tiles.get(P2::new(x as isize, y as isize)) { + tile.render(cell); + } else { + cell.set_symbol("."); + } + } + } + } +} diff --git a/src/geometry.rs b/src/geometry.rs index 9f6fac5..c6b22a9 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -1,64 +1,106 @@ use std::ops::{Add, Div, Sub}; #[derive(Clone, Copy, Debug, PartialEq)] -struct P2 { - x: i32, - y: i32, +pub struct P2 { + x: T, + y: T, } -impl P2 { - fn new(x: i32, y: i32) -> Self { +impl P2 { + pub fn new(x: T, y: T) -> Self { Self { x, y } } } -impl Add for P2 { - type Output = P2; - fn add(self, rhs: V2) -> Self::Output { +impl> Add> for P2 { + type Output = P2<::Output>; + + fn add(self, rhs: V2) -> Self::Output { P2::new(self.x + rhs.x, self.y + rhs.y) } } -impl Sub for P2 { - type Output = V2; +impl> Sub> for P2 { + type Output = V2<::Output>; - fn sub(self, rhs: P2) -> Self::Output { + fn sub(self, rhs: P2) -> Self::Output { V2::new(self.x - rhs.x, self.y - rhs.y) } } -impl Sub for P2 { - type Output = P2; +impl> Sub> for P2 { + type Output = P2<::Output>; - fn sub(self, rhs: V2) -> Self::Output { + fn sub(self, rhs: V2) -> Self::Output { P2::new(self.x - rhs.x, self.y - rhs.y) } } -impl Div for P2 { - type Output = P2; +impl> Div for P2 { + type Output = P2<::Output>; - fn div(self, rhs: i32) -> Self::Output { - Self::new(self.x / rhs, self.y / rhs) + fn div(self, rhs: T) -> Self::Output { + P2::new(self.x / rhs, self.y / rhs) } } #[derive(Debug)] -struct V2 { - x: i32, - y: i32, +pub struct V2 { + pub x: T, + pub y: T, } -impl V2 { - fn new(x: i32, y: i32) -> Self { +impl V2 { + pub fn new(x: T, y: T) -> Self { Self { x, y } } } -impl Div for V2 { - type Output = V2; +impl> Div for V2 { + type Output = V2<::Output>; - fn div(self, rhs: i32) -> Self::Output { + fn div(self, rhs: T) -> Self::Output { V2::new(self.x / rhs, self.y / rhs) } } + +pub struct Grid { + dims: V2, + elements: Vec, +} + +impl Grid { + pub fn from_fn(width: usize, height: usize, mut f: impl FnMut(usize, usize) -> T) -> Self { + let mut elements = Vec::new(); + for y in 0..height { + for x in 0..width { + elements.push(f(x, y)); + } + } + Self { + dims: V2::new(width, height), + elements, + } + } + + pub fn get(&self, index: P2) -> Option<&T> { + self.valid_index(index).map(|index| &self.elements[index]) + } + + pub fn get_mut(&mut self, index: P2) -> Option<&mut T> { + self.valid_index(index) + .map(|index| &mut self.elements[index]) + } + + fn valid_index(&self, index: P2) -> Option { + if index.x >= 0 + && (index.x as usize) < self.dims.x + && index.y >= 0 + && (index.y as usize) < self.dims.y + { + Some((index.y as usize) * self.dims.x + (index.x as usize)) + } else { + None + } + } +} diff --git a/src/main.rs b/src/main.rs index f457bdc..aa64287 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,31 @@ -use std::cmp::max; - +mod game; mod geometry; -mod noise; -mod terrain; -use noise::Noise; +use color_eyre::eyre::Result; +use crossterm::event; +use ratatui::DefaultTerminal; -fn main() { - let width = 50; - let height = 20; +use game::GameModel; - let long_side = max(width, height) as f32; - - let mut rand = rand::rng(); - let height_noise = Noise::from_freqs(&mut rand, 12, 16, 3); - let tree_cover_noise = Noise::from_freqs(&mut rand, 12, 16, 4); - - for y in 0..height { - for x in 0..width { - let h = height_noise.sample(x as f32 / long_side, y as f32 / long_side); - let t = tree_cover_noise.sample(x as f32 / long_side, y as f32 / long_side); - - let terrain = terrain::terrain(h, t); - print!("{}", terrain.printable()); - } - println!(); - } +fn main() -> Result<()> { + + let mut terminal = ratatui::init(); + let result = main_loop(&mut terminal); + ratatui::restore(); + result } + +fn main_loop(terminal: &mut DefaultTerminal) -> Result<()> { + let mut model = GameModel::new(); + loop { + terminal.draw(|frame| { + model.render(frame) + })?; + + let term_event = event::read()?; + match model.update(term_event) { + Some(new_model) => model = new_model, + None => break Ok(()), + } + } +} \ No newline at end of file