Merge branch 'dev' into 'main'
Merge Sprint 1 See merge request softwaregrundprojekt/2025-2026/einzelprojekt/tutorium-moritz/bernroider-dominik/bernroider-dominik!5
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
5618
Cargo.lock
generated
Normal file
5618
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "pomomon-garden"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 1
|
||||||
|
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 3
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1
|
||||||
|
lto = "thin"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bevy = "0.17.2"
|
||||||
|
bevy_aseprite_ultra = "0.7.0"
|
||||||
|
bevy_dev_tools = "0.17.2"
|
||||||
BIN
assets/pom/pom-idle.aseprite
Normal file
BIN
assets/pom/pom-idle.aseprite
Normal file
Binary file not shown.
BIN
assets/pom/pom-sleep.aseprite
Normal file
BIN
assets/pom/pom-sleep.aseprite
Normal file
Binary file not shown.
BIN
assets/pom/pom-walk.aseprite
Normal file
BIN
assets/pom/pom-walk.aseprite
Normal file
Binary file not shown.
BIN
assets/pom/pom.aseprite
Normal file
BIN
assets/pom/pom.aseprite
Normal file
Binary file not shown.
BIN
assets/tiles/tile-empty.aseprite
Normal file
BIN
assets/tiles/tile-empty.aseprite
Normal file
Binary file not shown.
BIN
assets/tiles/tile-occupied.aseprite
Normal file
BIN
assets/tiles/tile-occupied.aseprite
Normal file
Binary file not shown.
BIN
assets/tiles/tile-unclaimed.aseprite
Normal file
BIN
assets/tiles/tile-unclaimed.aseprite
Normal file
Binary file not shown.
116
flake.lock
generated
Normal file
116
flake.lock
generated
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1762538466,
|
||||||
|
"narHash": "sha256-8zrIPl6J+wLm9MH5ksHcW7BUHo7jSNOu0/hA0ohOOaM=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "0cea393fffb39575c46b7a0318386467272182fe",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fenix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1763361733,
|
||||||
|
"narHash": "sha256-ka7dpwH3HIXCyD2wl5F7cPLeRbqZoY2ullALsvxdPt8=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "6c8d48e3b0ae371b19ac1485744687b788e80193",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1760524057,
|
||||||
|
"narHash": "sha256-EVAqOteLBFmd7pKkb0+FIUyzTF61VKi7YmvP1tw4nEw=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "544961dfcce86422ba200ed9a0b00dd4b1486ec5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
|
"fenix": "fenix",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-analyzer-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1762860488,
|
||||||
|
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=",
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
120
flake.nix
Normal file
120
flake.nix
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
fenix = {
|
||||||
|
url = "github:nix-community/fenix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
crane.url = "github:ipetkov/crane";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
nixpkgs,
|
||||||
|
fenix,
|
||||||
|
crane,
|
||||||
|
flake-utils,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system: let
|
||||||
|
pkgs = import nixpkgs {inherit system;};
|
||||||
|
|
||||||
|
pname = "pomomon-garden";
|
||||||
|
version = "0.1.0";
|
||||||
|
|
||||||
|
rust-toolchain = with fenix.packages.${system};
|
||||||
|
combine [
|
||||||
|
beta.rustc
|
||||||
|
beta.cargo
|
||||||
|
beta.rust-src
|
||||||
|
beta.rust-analyzer
|
||||||
|
];
|
||||||
|
|
||||||
|
bevyDeps = with pkgs; [
|
||||||
|
pkg-config
|
||||||
|
# Audio
|
||||||
|
alsa-lib
|
||||||
|
# Vulkan
|
||||||
|
vulkan-loader
|
||||||
|
vulkan-tools
|
||||||
|
libudev-zero
|
||||||
|
# X11
|
||||||
|
xorg.libX11
|
||||||
|
xorg.libXcursor
|
||||||
|
xorg.libXi
|
||||||
|
xorg.libXrandr
|
||||||
|
# Wayland
|
||||||
|
wayland
|
||||||
|
libxkbcommon
|
||||||
|
# linker
|
||||||
|
lld
|
||||||
|
];
|
||||||
|
runtimeLibs = pkgs.lib.makeLibraryPath bevyDeps;
|
||||||
|
|
||||||
|
craneLib = (crane.mkLib pkgs).overrideToolchain rust-toolchain;
|
||||||
|
|
||||||
|
cargoArtifacts = craneLib.buildDepsOnly {
|
||||||
|
pname = "${pname}-deps";
|
||||||
|
src = craneLib.cleanCargoSource (craneLib.path ./.);
|
||||||
|
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;
|
||||||
|
src = craneLib.cleanCargoSource (craneLib.path ./.);
|
||||||
|
inherit cargoArtifacts;
|
||||||
|
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
pkg-config
|
||||||
|
rust-toolchain
|
||||||
|
lld
|
||||||
|
makeWrapper
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = bevyDeps;
|
||||||
|
|
||||||
|
inherit CARGO_PROFILE_RELEASE_LTO CARGO_PROFILE_RELEASE_CODEGEN_UNITS CARGO_PROFILE_RELEASE_STRIP RUSTFLAGS;
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
wrapProgram "$out/bin/${pname}" \
|
||||||
|
--prefix LD_LIBRARY_PATH : ${runtimeLibs}
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = "A pomodoro garden management game.";
|
||||||
|
license = licenses.mit;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
pkg-config
|
||||||
|
lld
|
||||||
|
];
|
||||||
|
|
||||||
|
packages = with pkgs;
|
||||||
|
[
|
||||||
|
rust-toolchain
|
||||||
|
cargo-watch
|
||||||
|
cargo-edit
|
||||||
|
cargo-tarpaulin
|
||||||
|
]
|
||||||
|
++ 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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
formatter = pkgs.nixpkgs-fmt;
|
||||||
|
});
|
||||||
|
}
|
||||||
2
src/components/mod.rs
Normal file
2
src/components/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod pom;
|
||||||
|
pub mod tile;
|
||||||
39
src/components/pom.rs
Normal file
39
src/components/pom.rs
Normal file
@@ -0,0 +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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/components/tile.rs
Normal file
64
src/components/tile.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::errors::GridError;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Tile {
|
||||||
|
pub x: u32,
|
||||||
|
pub y: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
pub enum TileState {
|
||||||
|
#[default]
|
||||||
|
Unclaimed,
|
||||||
|
Empty,
|
||||||
|
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<Vec<Entity>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Grid {
|
||||||
|
pub fn get_tile(&self, pos: (u32, u32)) -> Result<Entity, GridError> {
|
||||||
|
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<F>(
|
||||||
|
&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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/errors.rs
Normal file
15
src/errors.rs
Normal file
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/lib.rs
Normal file
6
src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
pub mod components;
|
||||||
|
pub mod errors;
|
||||||
|
pub mod messages;
|
||||||
|
pub mod plugins;
|
||||||
|
pub mod states;
|
||||||
|
pub mod utils;
|
||||||
33
src/main.rs
Normal file
33
src/main.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_aseprite_ultra::prelude::*;
|
||||||
|
use bevy_dev_tools::fps_overlay::*;
|
||||||
|
use pomomon_garden::plugins;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins((
|
||||||
|
DefaultPlugins.set(ImagePlugin::default_nearest()),
|
||||||
|
AsepriteUltraPlugin,
|
||||||
|
))
|
||||||
|
.add_plugins((FpsOverlayPlugin {
|
||||||
|
config: FpsOverlayConfig {
|
||||||
|
refresh_interval: core::time::Duration::from_millis(100),
|
||||||
|
enabled: true,
|
||||||
|
frame_time_graph_config: FrameTimeGraphConfig {
|
||||||
|
enabled: true,
|
||||||
|
min_fps: 30.0,
|
||||||
|
target_fps: 144.0,
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
},))
|
||||||
|
.add_plugins((
|
||||||
|
plugins::CorePlugin,
|
||||||
|
plugins::StartScreenPlugin,
|
||||||
|
plugins::GameScreenPlugin,
|
||||||
|
plugins::GridPlugin,
|
||||||
|
plugins::PomPlugin,
|
||||||
|
plugins::InputPlugin,
|
||||||
|
))
|
||||||
|
.run();
|
||||||
|
}
|
||||||
18
src/messages.rs
Normal file
18
src/messages.rs
Normal file
@@ -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,
|
||||||
|
}
|
||||||
15
src/plugins/core.rs
Normal file
15
src/plugins/core.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use crate::states::*;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub struct CorePlugin;
|
||||||
|
|
||||||
|
impl Plugin for CorePlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(Startup, setup_camera);
|
||||||
|
app.init_state::<AppState>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_camera(mut commands: Commands) {
|
||||||
|
commands.spawn(Camera2d::default());
|
||||||
|
}
|
||||||
19
src/plugins/game_screen.rs
Normal file
19
src/plugins/game_screen.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use crate::states::*;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub struct GameScreenPlugin;
|
||||||
|
|
||||||
|
impl Plugin for GameScreenPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(OnEnter(AppState::GameScreen), setup);
|
||||||
|
app.add_systems(OnExit(AppState::GameScreen), cleanup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(mut commands: Commands) {
|
||||||
|
commands.insert_resource(ClearColor(Color::srgb(0.294, 0.412, 0.184)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(mut commands: Commands) {
|
||||||
|
commands.remove_resource::<ClearColor>();
|
||||||
|
}
|
||||||
110
src/plugins/grid.rs
Normal file
110
src/plugins/grid.rs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
use crate::{
|
||||||
|
components::tile::{Grid, Tile, TileState},
|
||||||
|
states::AppState,
|
||||||
|
};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_aseprite_ultra::prelude::AseSlice;
|
||||||
|
|
||||||
|
pub const TILE_SIZE: f32 = 32.0;
|
||||||
|
pub const GRID_WIDTH: u32 = 10;
|
||||||
|
pub const GRID_HEIGHT: u32 = 10;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
impl Plugin for GridPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(OnEnter(AppState::GameScreen), setup);
|
||||||
|
app.add_systems(OnExit(AppState::GameScreen), cleanup);
|
||||||
|
|
||||||
|
app.add_systems(
|
||||||
|
Update,
|
||||||
|
update_tile_colors.run_if(in_state(AppState::GameScreen)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
|
let mut tiles = Vec::with_capacity(GRID_WIDTH as usize);
|
||||||
|
|
||||||
|
for x in 0..GRID_WIDTH {
|
||||||
|
let mut column = Vec::with_capacity(GRID_HEIGHT as usize);
|
||||||
|
|
||||||
|
for y in 0..GRID_HEIGHT {
|
||||||
|
let tile_entity = commands
|
||||||
|
.spawn((
|
||||||
|
Tile { x, y },
|
||||||
|
TileState::Unclaimed,
|
||||||
|
AseSlice {
|
||||||
|
name: "Unclaimed".into(),
|
||||||
|
aseprite: asset_server.load("tiles/tile-unclaimed.aseprite"),
|
||||||
|
},
|
||||||
|
Sprite::default(),
|
||||||
|
Transform::from_translation(grid_to_world_coords(x, y, None)),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
column.push(tile_entity);
|
||||||
|
}
|
||||||
|
tiles.push(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
commands.insert_resource(Grid {
|
||||||
|
width: GRID_WIDTH,
|
||||||
|
height: GRID_HEIGHT,
|
||||||
|
tiles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(mut commands: Commands, tile_query: Query<Entity, With<Tile>>) {
|
||||||
|
for tile_entity in tile_query.iter() {
|
||||||
|
commands.entity(tile_entity).despawn();
|
||||||
|
}
|
||||||
|
commands.remove_resource::<Grid>();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_tile_colors(
|
||||||
|
mut query: Query<(&TileState, &mut AseSlice)>,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
) {
|
||||||
|
for (state, mut slice) in &mut query {
|
||||||
|
slice.name = match state {
|
||||||
|
TileState::Unclaimed => "Unclaimed",
|
||||||
|
TileState::Empty => "Empty",
|
||||||
|
TileState::Occupied => "Occupied",
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
|
||||||
|
slice.aseprite = match state {
|
||||||
|
TileState::Unclaimed => asset_server.load("tiles/tile-unclaimed.aseprite"),
|
||||||
|
TileState::Empty => asset_server.load("tiles/tile-empty.aseprite"),
|
||||||
|
TileState::Occupied => asset_server.load("tiles/tile-occupied.aseprite"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<f32>) -> 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),
|
||||||
|
)
|
||||||
|
}
|
||||||
84
src/plugins/input.rs
Normal file
84
src/plugins/input.rs
Normal file
@@ -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::<MoveMessage>();
|
||||||
|
app.add_message::<InvalidMoveMessage>();
|
||||||
|
app.add_systems(Update, move_click.run_if(in_state(AppState::GameScreen)));
|
||||||
|
|
||||||
|
app.add_message::<InteractStartMessage>();
|
||||||
|
app.add_systems(
|
||||||
|
Update,
|
||||||
|
interact_click.run_if(in_state(AppState::GameScreen)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_click(
|
||||||
|
mut move_messages: MessageWriter<MoveMessage>,
|
||||||
|
mouse_btn: Res<ButtonInput<MouseButton>>,
|
||||||
|
window: Single<&Window, With<PrimaryWindow>>,
|
||||||
|
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
||||||
|
) {
|
||||||
|
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<InteractStartMessage>,
|
||||||
|
mouse_btn: Res<ButtonInput<MouseButton>>,
|
||||||
|
window: Single<&Window, With<PrimaryWindow>>,
|
||||||
|
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
||||||
|
// for debug
|
||||||
|
grid: ResMut<Grid>,
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/plugins/mod.rs
Normal file
13
src/plugins/mod.rs
Normal file
@@ -0,0 +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;
|
||||||
145
src/plugins/pom.rs
Normal file
145
src/plugins/pom.rs
Normal file
@@ -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<AssetServer>) {
|
||||||
|
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<Entity, With<Pom>>) {
|
||||||
|
for pom_entity in pom_query.iter() {
|
||||||
|
commands.entity(pom_entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_move(
|
||||||
|
mut move_messages: MessageReader<MoveMessage>,
|
||||||
|
mut invalid_move_messages: MessageWriter<InvalidMoveMessage>,
|
||||||
|
grid: Res<Grid>,
|
||||||
|
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<Time>,
|
||||||
|
mut query: Query<(
|
||||||
|
&mut Transform,
|
||||||
|
&mut GridPosition,
|
||||||
|
&mut PathQueue,
|
||||||
|
&mut MovingState,
|
||||||
|
)>,
|
||||||
|
) {
|
||||||
|
let dt = time.delta_secs();
|
||||||
|
|
||||||
|
for (mut transform, mut grid_pos, mut path_queue, mut moving_state) in query.iter_mut() {
|
||||||
|
if let Some(&target) = path_queue.steps.front() {
|
||||||
|
let target_pos = grid_to_world_coords(target.0, target.1, Some(1.0));
|
||||||
|
let distance = transform.translation.distance(target_pos);
|
||||||
|
|
||||||
|
let dx = target.0 as i32 - grid_pos.x as i32;
|
||||||
|
let dy = target.1 as i32 - grid_pos.y as i32;
|
||||||
|
match (dx, dy) {
|
||||||
|
(0, 1) => *moving_state = MovingState::MovingUp,
|
||||||
|
(0, -1) => *moving_state = MovingState::MovingDown,
|
||||||
|
(1, 0) => *moving_state = MovingState::MovingRight,
|
||||||
|
(-1, 0) => *moving_state = MovingState::MovingLeft,
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
if distance < MOVE_SPEED * dt {
|
||||||
|
transform.translation = target_pos;
|
||||||
|
grid_pos.x = target.0;
|
||||||
|
grid_pos.y = target.1;
|
||||||
|
path_queue.steps.pop_front();
|
||||||
|
} else {
|
||||||
|
let direction = (target_pos - transform.translation).normalize();
|
||||||
|
transform.translation += direction * MOVE_SPEED * dt;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*moving_state = MovingState::Idle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_pom(asset_server: Res<AssetServer>, mut query: Query<(&MovingState, &mut AseAnimation)>) {
|
||||||
|
for (moving_state, mut animation) in query.iter_mut() {
|
||||||
|
match moving_state {
|
||||||
|
MovingState::Idle => {
|
||||||
|
*animation = AseAnimation {
|
||||||
|
aseprite: asset_server.load("pom/pom-sleep.aseprite"),
|
||||||
|
animation: Animation::tag("sleep-sit-start").with_repeat(AnimationRepeat::Loop),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
s if s.is_moving() => {
|
||||||
|
*animation = AseAnimation {
|
||||||
|
aseprite: asset_server.load("pom/pom-walk.aseprite"),
|
||||||
|
animation: (match s {
|
||||||
|
MovingState::MovingUp => Animation::tag("walk_up"),
|
||||||
|
MovingState::MovingDown => Animation::tag("walk_down"),
|
||||||
|
MovingState::MovingLeft => Animation::tag("walk_left"),
|
||||||
|
MovingState::MovingRight => Animation::tag("walk_right"),
|
||||||
|
_ => panic!("This shouldn't be reachable"),
|
||||||
|
})
|
||||||
|
.with_repeat(AnimationRepeat::Loop),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
146
src/plugins/start_screen.rs
Normal file
146
src/plugins/start_screen.rs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
use crate::states::*;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub struct StartScreenPlugin;
|
||||||
|
|
||||||
|
impl Plugin for StartScreenPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(OnEnter(AppState::StartScreen), setup);
|
||||||
|
app.add_systems(OnExit(AppState::StartScreen), cleanup);
|
||||||
|
app.add_systems(Update, menu.run_if(in_state(AppState::StartScreen)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct MenuData {
|
||||||
|
button_entity: Entity,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
enum ButtonType {
|
||||||
|
LoadGame,
|
||||||
|
NewGame,
|
||||||
|
Settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
|
||||||
|
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
|
||||||
|
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
|
||||||
|
|
||||||
|
fn setup(mut commands: Commands) {
|
||||||
|
let button_entity = commands
|
||||||
|
.spawn((
|
||||||
|
Node {
|
||||||
|
width: percent(100),
|
||||||
|
height: percent(100),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
children![
|
||||||
|
(
|
||||||
|
Text::new("Pomonon Garten"),
|
||||||
|
TextFont {
|
||||||
|
font_size: 64.0,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Button,
|
||||||
|
ButtonType::LoadGame,
|
||||||
|
Node {
|
||||||
|
width: px(300),
|
||||||
|
height: px(65),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(NORMAL_BUTTON),
|
||||||
|
children![(
|
||||||
|
Text::new("Spiel laden"),
|
||||||
|
TextFont {
|
||||||
|
font_size: 33.0,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||||
|
)]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Button,
|
||||||
|
ButtonType::NewGame,
|
||||||
|
Node {
|
||||||
|
width: px(300),
|
||||||
|
height: px(65),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(NORMAL_BUTTON),
|
||||||
|
children![(
|
||||||
|
Text::new("Neues Spiel"),
|
||||||
|
TextFont {
|
||||||
|
font_size: 33.0,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||||
|
)]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Button,
|
||||||
|
ButtonType::Settings,
|
||||||
|
Node {
|
||||||
|
width: px(300),
|
||||||
|
height: px(65),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(NORMAL_BUTTON),
|
||||||
|
children![(
|
||||||
|
Text::new("Einstellungen"),
|
||||||
|
TextFont {
|
||||||
|
font_size: 33.0,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
|
||||||
|
commands.insert_resource(MenuData { button_entity });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn menu(
|
||||||
|
mut next_state: ResMut<NextState<AppState>>,
|
||||||
|
mut interaction_query: Query<
|
||||||
|
(&Interaction, &ButtonType, &mut BackgroundColor),
|
||||||
|
(Changed<Interaction>, With<Button>),
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
for (interaction, button_type, mut color) in &mut interaction_query {
|
||||||
|
match *interaction {
|
||||||
|
Interaction::Pressed => {
|
||||||
|
*color = PRESSED_BUTTON.into();
|
||||||
|
|
||||||
|
match button_type {
|
||||||
|
ButtonType::NewGame => next_state.set(AppState::GameScreen),
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Interaction::Hovered => {
|
||||||
|
*color = HOVERED_BUTTON.into();
|
||||||
|
}
|
||||||
|
Interaction::None => {
|
||||||
|
*color = NORMAL_BUTTON.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(mut commands: Commands, menu_data: Res<MenuData>) {
|
||||||
|
commands.entity(menu_data.button_entity).despawn();
|
||||||
|
}
|
||||||
8
src/states.rs
Normal file
8
src/states.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
#[derive(States, Clone, PartialEq, Eq, Debug, Hash, Default, Reflect)]
|
||||||
|
pub enum AppState {
|
||||||
|
#[default]
|
||||||
|
StartScreen,
|
||||||
|
GameScreen,
|
||||||
|
}
|
||||||
1
src/utils/mod.rs
Normal file
1
src/utils/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod pathfinding;
|
||||||
107
src/utils/pathfinding.rs
Normal file
107
src/utils/pathfinding.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
use crate::components::tile::{Grid, TileState};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::collections::{BinaryHeap, HashMap, VecDeque};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub struct Node {
|
||||||
|
x: u32,
|
||||||
|
y: u32,
|
||||||
|
cost: u32, // G score (distance from start)
|
||||||
|
priority: u32, // F score (G + H)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Node {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
other
|
||||||
|
.priority
|
||||||
|
.cmp(&self.priority)
|
||||||
|
.then_with(|| self.cost.cmp(&other.cost))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Node {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn manhattan_distance(x1: u32, y1: u32, x2: u32, y2: u32) -> u32 {
|
||||||
|
(x1 as i32 - x2 as i32).abs() as u32 + (y1 as i32 - y2 as i32).abs() as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_path(
|
||||||
|
start: (u32, u32),
|
||||||
|
end: (u32, u32),
|
||||||
|
grid: &Grid,
|
||||||
|
tile_query: &Query<&TileState>,
|
||||||
|
) -> Option<VecDeque<(u32, u32)>> {
|
||||||
|
println!("find_path");
|
||||||
|
|
||||||
|
let target_entity = grid.get_tile(end).ok()?;
|
||||||
|
let state = tile_query.get(target_entity).unwrap();
|
||||||
|
if (*state).is_blocking() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut open_set = BinaryHeap::new();
|
||||||
|
let mut came_from: HashMap<(u32, u32), (u32, u32)> = HashMap::new();
|
||||||
|
let mut g_score: HashMap<(u32, u32), u32> = HashMap::new();
|
||||||
|
|
||||||
|
g_score.insert(start, 0);
|
||||||
|
open_set.push(Node {
|
||||||
|
x: start.0,
|
||||||
|
y: start.1,
|
||||||
|
cost: 0,
|
||||||
|
priority: manhattan_distance(start.0, start.1, end.0, end.1),
|
||||||
|
});
|
||||||
|
|
||||||
|
while let Some(current) = open_set.pop() {
|
||||||
|
let current_pos = (current.x, current.y);
|
||||||
|
|
||||||
|
if current_pos == end {
|
||||||
|
let mut path = VecDeque::new();
|
||||||
|
let mut curr = end;
|
||||||
|
while curr != start {
|
||||||
|
path.push_front(curr);
|
||||||
|
curr = *came_from.get(&curr).unwrap();
|
||||||
|
}
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let neighbors = [
|
||||||
|
(current.x as i32 + 1, current.y as i32),
|
||||||
|
(current.x as i32 - 1, current.y as i32),
|
||||||
|
(current.x as i32, current.y as i32 + 1),
|
||||||
|
(current.x as i32, current.y as i32 - 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (nx, ny) in neighbors {
|
||||||
|
if nx < 0 || ny < 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let next_pos = (nx as u32, ny as u32);
|
||||||
|
|
||||||
|
let tile_entity = grid.get_tile(next_pos).ok()?;
|
||||||
|
let state = tile_query.get(tile_entity).unwrap();
|
||||||
|
if (*state).is_blocking() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tentative_g_score = g_score.get(¤t_pos).unwrap() + 1;
|
||||||
|
if tentative_g_score < *g_score.get(&next_pos).unwrap_or(&u32::MAX) {
|
||||||
|
came_from.insert(next_pos, current_pos);
|
||||||
|
g_score.insert(next_pos, tentative_g_score);
|
||||||
|
open_set.push(Node {
|
||||||
|
x: next_pos.0,
|
||||||
|
y: next_pos.1,
|
||||||
|
cost: tentative_g_score,
|
||||||
|
priority: tentative_g_score
|
||||||
|
+ manhattan_distance(next_pos.0, next_pos.1, end.0, end.1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user