From f3e8dd2b35351ad7b1f7702cb4f7b93dda4428f1 Mon Sep 17 00:00:00 2001 From: demenik Date: Fri, 21 Nov 2025 14:30:31 +0100 Subject: [PATCH 1/3] feat: Add Pom walking sprites (#20) --- assets/pom/pom-walk.aseprite | Bin 0 -> 4519 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/pom/pom-walk.aseprite diff --git a/assets/pom/pom-walk.aseprite b/assets/pom/pom-walk.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..8947d1b4538320516544c34015ec1ddcdbcdf96a GIT binary patch literal 4519 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 literal 0 HcmV?d00001 From 6ed2adc43b9c808e34112ce5fe60d33d997b85f2 Mon Sep 17 00:00:00 2001 From: demenik Date: Fri, 21 Nov 2025 14:32:27 +0100 Subject: [PATCH 2/3] chore: Update flake.nix build system --- flake.nix | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 From 395941508f1aa3b8644c62f0201d2cd1b59e0210 Mon Sep 17 00:00:00 2001 From: demenik Date: Fri, 21 Nov 2025 14:33:30 +0100 Subject: [PATCH 3/3] feat: Implement Pom and Pom movement (#16, #20) --- src/components/pom.rs | 35 +++++++++ src/components/tile.rs | 43 ++++++++++- src/errors.rs | 15 ++++ src/lib.rs | 3 + src/main.rs | 2 + src/messages.rs | 18 +++++ src/plugins/game_screen.rs | 14 +--- src/plugins/grid.rs | 41 ++++++++--- src/plugins/input.rs | 84 +++++++++++++++++++++ src/plugins/mod.rs | 4 + src/plugins/pom.rs | 145 +++++++++++++++++++++++++++++++++++++ src/utils/mod.rs | 1 + src/utils/pathfinding.rs | 107 +++++++++++++++++++++++++++ 13 files changed, 488 insertions(+), 24 deletions(-) create mode 100644 src/errors.rs create mode 100644 src/messages.rs create mode 100644 src/plugins/input.rs create mode 100644 src/plugins/pom.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/pathfinding.rs 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