diff --git a/assets/pom/pom-walk.aseprite b/assets/pom/pom-walk.aseprite new file mode 100644 index 0000000..8947d1b Binary files /dev/null and b/assets/pom/pom-walk.aseprite differ diff --git a/flake.nix b/flake.nix index 7e74b4b..e054130 100644 --- a/flake.nix +++ b/flake.nix @@ -59,6 +59,11 @@ nativeBuildInputs = with pkgs; [pkg-config]; buildInputs = bevyDeps; }; + + CARGO_PROFILE_RELEASE_LTO = "thin"; + CARGO_PROFILE_RELEASE_CODEGEN_UNITS = 1; + CARGO_PROFILE_RELEASE_STRIP = "true"; + RUSTFLAGS = "-C link-arg=-fuse-ld=lld"; in { packages.default = craneLib.buildPackage { inherit pname version; @@ -74,10 +79,7 @@ buildInputs = bevyDeps; - CARGO_PROFILE_RELEASE_LTO = "thin"; - CARGO_PROFILE_RELEASE_CODEGEN_UNITS = 1; - CARGO_PROFILE_RELEASE_STRIP = true; - RUSTFLAGS = "-C link-arg=-fuse-ld=lld"; + inherit CARGO_PROFILE_RELEASE_LTO CARGO_PROFILE_RELEASE_CODEGEN_UNITS CARGO_PROFILE_RELEASE_STRIP RUSTFLAGS; postInstall = '' wrapProgram "$out/bin/${pname}" \ @@ -105,6 +107,8 @@ ] ++ bevyDeps; + inherit CARGO_PROFILE_RELEASE_LTO CARGO_PROFILE_RELEASE_CODEGEN_UNITS CARGO_PROFILE_RELEASE_STRIP RUSTFLAGS; + shellHook = '' export RUST_SRC_PATH=${fenix.packages.${system}.stable.rust-src}/lib/rustlib/src/rust/library export LD_LIBRARY_PATH=${runtimeLibs}:$LD_LIBRARY_PATH diff --git a/src/components/pom.rs b/src/components/pom.rs index 611c0a6..469e036 100644 --- a/src/components/pom.rs +++ b/src/components/pom.rs @@ -1,4 +1,39 @@ +use std::collections::VecDeque; + use bevy::prelude::*; #[derive(Component)] pub struct Pom; + +#[derive(Component)] +pub struct GridPosition { + pub x: u32, + pub y: u32, +} + +#[derive(Component, Default)] +pub struct PathQueue { + pub steps: VecDeque<(u32, u32)>, +} + +#[derive(Component, Default)] +pub enum MovingState { + #[default] + Idle, + MovingUp, + MovingDown, + MovingLeft, + MovingRight, +} + +impl MovingState { + pub fn is_moving(&self) -> bool { + matches!( + self, + MovingState::MovingUp + | MovingState::MovingDown + | MovingState::MovingLeft + | MovingState::MovingRight + ) + } +} diff --git a/src/components/tile.rs b/src/components/tile.rs index 323b12e..12d7376 100644 --- a/src/components/tile.rs +++ b/src/components/tile.rs @@ -1,5 +1,6 @@ use bevy::prelude::*; -use bevy_aseprite_ultra::prelude::AseSlice; + +use crate::errors::GridError; #[derive(Component)] pub struct Tile { @@ -15,9 +16,49 @@ pub enum TileState { Occupied, } +impl TileState { + pub fn is_blocking(&self) -> bool { + match self { + TileState::Occupied => true, + _ => false, + } + } +} + #[derive(Resource)] pub struct Grid { pub width: u32, pub height: u32, pub tiles: Vec>, } + +impl Grid { + pub fn get_tile(&self, pos: (u32, u32)) -> Result { + if pos.0 >= self.width || pos.1 >= self.height { + return Err(GridError::OutOfBounds { + x: pos.0 as i32, + y: pos.1 as i32, + }); + } + Ok(self.tiles[pos.0 as usize][pos.1 as usize]) + } + + pub fn map_tile_state( + &self, + pos: (u32, u32), + mapper: F, + mut tile_query: Query<&mut TileState>, + ) -> Result<(), GridError> + where + F: FnOnce(&TileState) -> TileState, + { + let tile_entity = self.get_tile(pos)?; + + let mut tile_state = tile_query + .get_mut(tile_entity) + .map_err(|_| GridError::UnknownError)?; + + *tile_state = mapper(&*tile_state); + Ok(()) + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..fc77c3c --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,15 @@ +use std::{error::Error, fmt}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GridError { + OutOfBounds { x: i32, y: i32 }, + UnknownError, +} + +impl Error for GridError {} + +impl fmt::Display for GridError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "GridError: {}", &self.to_string()) + } +} diff --git a/src/lib.rs b/src/lib.rs index ea55069..c45b318 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ pub mod components; +pub mod errors; +pub mod messages; pub mod plugins; pub mod states; +pub mod utils; diff --git a/src/main.rs b/src/main.rs index e0a805b..12401b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,8 @@ fn main() { plugins::StartScreenPlugin, plugins::GameScreenPlugin, plugins::GridPlugin, + plugins::PomPlugin, + plugins::InputPlugin, )) .run(); } diff --git a/src/messages.rs b/src/messages.rs new file mode 100644 index 0000000..bd71c95 --- /dev/null +++ b/src/messages.rs @@ -0,0 +1,18 @@ +use bevy::prelude::*; + +#[derive(Message)] +pub struct MoveMessage { + pub x: u32, + pub y: u32, +} + +#[derive(Message)] +pub struct InvalidMoveMessage { + pub message: String, +} + +#[derive(Message)] +pub struct InteractStartMessage { + pub x: u32, + pub y: u32, +} diff --git a/src/plugins/game_screen.rs b/src/plugins/game_screen.rs index 7134098..6a6e656 100644 --- a/src/plugins/game_screen.rs +++ b/src/plugins/game_screen.rs @@ -1,7 +1,5 @@ -use crate::components::*; use crate::states::*; use bevy::prelude::*; -use bevy_aseprite_ultra::prelude::*; pub struct GameScreenPlugin; @@ -12,17 +10,7 @@ impl Plugin for GameScreenPlugin { } } -fn setup(mut commands: Commands, asset_server: Res) { - commands.spawn(( - pom::Pom, - AseAnimation { - aseprite: asset_server.load("pom/pom-sleep.aseprite"), - animation: Animation::tag("sleep-sit-start").with_repeat(AnimationRepeat::Loop), - }, - Sprite::default(), - Transform::from_xyz(0.0, 0.0, 1.0), - )); - +fn setup(mut commands: Commands) { commands.insert_resource(ClearColor(Color::srgb(0.294, 0.412, 0.184))); } diff --git a/src/plugins/grid.rs b/src/plugins/grid.rs index 1644bea..d8a4f75 100644 --- a/src/plugins/grid.rs +++ b/src/plugins/grid.rs @@ -5,12 +5,12 @@ use crate::{ use bevy::prelude::*; use bevy_aseprite_ultra::prelude::AseSlice; -const TILE_SIZE: f32 = 32.0; -const GRID_WIDTH: u32 = 10; -const GRID_HEIGHT: u32 = 10; +pub const TILE_SIZE: f32 = 32.0; +pub const GRID_WIDTH: u32 = 10; +pub const GRID_HEIGHT: u32 = 10; -const GRID_START_X: f32 = -(GRID_WIDTH as f32 * TILE_SIZE) / 2.0 + TILE_SIZE / 2.0; -const GRID_START_Y: f32 = -(GRID_HEIGHT as f32 * TILE_SIZE) / 2.0 + TILE_SIZE / 2.0; +pub const GRID_START_X: f32 = -(GRID_WIDTH as f32 * TILE_SIZE) / 2.0 + TILE_SIZE / 2.0; +pub const GRID_START_Y: f32 = -(GRID_HEIGHT as f32 * TILE_SIZE) / 2.0 + TILE_SIZE / 2.0; pub struct GridPlugin; @@ -42,11 +42,7 @@ fn setup(mut commands: Commands, asset_server: Res) { aseprite: asset_server.load("tiles/tile-unclaimed.aseprite"), }, Sprite::default(), - Transform::from_xyz( - GRID_START_X + x as f32 * TILE_SIZE, - GRID_START_Y + y as f32 * TILE_SIZE, - 0.0, - ), + Transform::from_translation(grid_to_world_coords(x, y, None)), )) .id(); column.push(tile_entity); @@ -87,3 +83,28 @@ fn update_tile_colors( }; } } + +pub fn world_to_grid_coords(world_pos: Vec3) -> (u32, u32) { + let x = ((world_pos.x - GRID_START_X + TILE_SIZE / 2.0) / TILE_SIZE).floor(); + let y = ((world_pos.y - GRID_START_Y + TILE_SIZE / 2.0) / TILE_SIZE).floor(); + + let mut x_u32 = x as u32; + let mut y_u32 = y as u32; + + if x_u32 >= GRID_WIDTH { + x_u32 = GRID_WIDTH - 1; + } + if y_u32 >= GRID_HEIGHT { + y_u32 = GRID_HEIGHT - 1; + } + + (x_u32, y_u32) +} + +pub fn grid_to_world_coords(grid_x: u32, grid_y: u32, z: Option) -> Vec3 { + Vec3::new( + GRID_START_X + grid_x as f32 * TILE_SIZE, + GRID_START_Y + grid_y as f32 * TILE_SIZE, + z.unwrap_or(0.0), + ) +} diff --git a/src/plugins/input.rs b/src/plugins/input.rs new file mode 100644 index 0000000..c16b1ad --- /dev/null +++ b/src/plugins/input.rs @@ -0,0 +1,84 @@ +use bevy::input::mouse::MouseButton; +use bevy::prelude::*; +use bevy::window::PrimaryWindow; + +use crate::components::tile::{Grid, TileState}; +use crate::messages::{InteractStartMessage, InvalidMoveMessage, MoveMessage}; +use crate::plugins::grid::world_to_grid_coords; +use crate::states::AppState; + +pub struct InputPlugin; + +impl Plugin for InputPlugin { + fn build(&self, app: &mut App) { + app.add_message::(); + app.add_message::(); + app.add_systems(Update, move_click.run_if(in_state(AppState::GameScreen))); + + app.add_message::(); + app.add_systems( + Update, + interact_click.run_if(in_state(AppState::GameScreen)), + ); + } +} + +fn move_click( + mut move_messages: MessageWriter, + mouse_btn: Res>, + window: Single<&Window, With>, + camera: Single<(&Camera, &GlobalTransform), With>, +) { + if mouse_btn.just_pressed(MouseButton::Right) { + let (cam, cam_transform) = *camera; + + let Some(cursor_pos) = window.cursor_position() else { + return; + }; + let Ok(world_pos) = cam.viewport_to_world(cam_transform, cursor_pos) else { + return; + }; + let (x, y) = world_to_grid_coords(world_pos.origin); + + println!("Move Click: ({}, {})", x, y); + move_messages.write(MoveMessage { x, y }); + } +} + +fn interact_click( + mut interact_messages: MessageWriter, + mouse_btn: Res>, + window: Single<&Window, With>, + camera: Single<(&Camera, &GlobalTransform), With>, + // for debug + grid: ResMut, + tile_query: Query<&mut TileState>, +) { + if mouse_btn.just_pressed(MouseButton::Left) { + let (cam, cam_transform) = *camera; + + let Some(cursor_pos) = window.cursor_position() else { + return; + }; + let Ok(world_pos) = cam.viewport_to_world(cam_transform, cursor_pos) else { + return; + }; + let (x, y) = world_to_grid_coords(world_pos.origin); + + println!("Interact Click: ({}, {})", x, y); + interact_messages.write(InteractStartMessage { x, y }); + + if cfg!(debug_assertions) { + grid.map_tile_state( + (x, y), + |state| match state { + TileState::Unclaimed => TileState::Empty, + TileState::Empty => TileState::Occupied, + TileState::Occupied => TileState::Unclaimed, + }, + tile_query, + ) + .unwrap(); + } + } +} diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index 9f2400c..314256f 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -1,9 +1,13 @@ pub mod core; pub mod game_screen; pub mod grid; +pub mod input; +pub mod pom; pub mod start_screen; pub use core::CorePlugin; pub use game_screen::GameScreenPlugin; pub use grid::GridPlugin; +pub use input::InputPlugin; +pub use pom::PomPlugin; pub use start_screen::StartScreenPlugin; diff --git a/src/plugins/pom.rs b/src/plugins/pom.rs new file mode 100644 index 0000000..64b9b5b --- /dev/null +++ b/src/plugins/pom.rs @@ -0,0 +1,145 @@ +use crate::components::pom::{GridPosition, MovingState, PathQueue, Pom}; +use crate::components::tile::{Grid, TileState}; +use crate::messages::{InvalidMoveMessage, MoveMessage}; +use crate::plugins::grid::{GRID_WIDTH, grid_to_world_coords}; +use crate::states::*; +use crate::utils::pathfinding::find_path; +use bevy::prelude::*; +use bevy_aseprite_ultra::prelude::*; + +const MOVE_SPEED: f32 = 2.0 * GRID_WIDTH as f32; + +pub struct PomPlugin; + +impl Plugin for PomPlugin { + fn build(&self, app: &mut App) { + app.add_systems(OnEnter(AppState::GameScreen), setup); + app.add_systems(OnExit(AppState::GameScreen), cleanup); + + app.add_systems( + Update, + (handle_move, move_pom, update_pom).run_if(in_state(AppState::GameScreen)), + ); + } +} + +fn setup(mut commands: Commands, asset_server: Res) { + commands.spawn(( + Pom, + GridPosition { x: 0, y: 0 }, + PathQueue::default(), + MovingState::default(), + AseAnimation { + aseprite: asset_server.load("pom/pom-sleep.aseprite"), + animation: Animation::tag("sleep-sit-start").with_repeat(AnimationRepeat::Loop), + }, + Sprite::default(), + Transform::from_translation(grid_to_world_coords(0, 0, Some(1.0))), + )); +} + +fn cleanup(mut commands: Commands, pom_query: Query>) { + for pom_entity in pom_query.iter() { + commands.entity(pom_entity).despawn(); + } +} + +fn handle_move( + mut move_messages: MessageReader, + mut invalid_move_messages: MessageWriter, + grid: Res, + tile_query: Query<&TileState>, + mut pom_query: Query<(&GridPosition, &mut PathQueue)>, +) { + for message in move_messages.read() { + for (grid_pos, mut path_queue) in pom_query.iter_mut() { + let start = (grid_pos.x, grid_pos.y); + let end = (message.x, message.y); + + println!("{}, {}", end.0, end.1); + + match find_path(start, end, &grid, &tile_query) { + Some(new_path) => { + path_queue.steps = new_path; + println!("Path found with {} steps", path_queue.steps.len()); + } + None => { + let msg = format!( + "Cannot move to ({}, {}). Path blocked or invalid.", + message.x, message.y + ); + println!("{}", msg); + invalid_move_messages.write(InvalidMoveMessage { message: msg }); + } + } + } + } +} + +fn move_pom( + time: Res