[no description]
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | Cargo.toml | 22 | ||||
| -rw-r--r-- | assets/tiles.png | bin | 0 -> 88859 bytes | |||
| -rw-r--r-- | src/main.rs | 239 | ||||
| -rw-r--r-- | src/picking.rs | 106 |
5 files changed, 369 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f2205dd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "bevinator" +version = "0.1.0" +edition = "2024" + + +[dependencies] +aoc = { version = "0.1.0", path = "../aoc" } +bevy = { version = "0.19.0", features = ["dynamic_linking"] } +bevy-inspector-egui = "0.37.0" +bevy_ecs_tilemap = { git = "https://github.com/taboky-dev/bevy_ecs_tilemap", branch = "bevy-0.19" } +rand = "0.10.1" + +[profile.dev] +opt-level = 1 +overflow-checks= false + +[profile.dev.package."*"] +opt-level = 3 + +[patch.crates-io] +rustc-hash = { git = "https://github.com/rust-lang/rustc-hash.git", rev= "1a998d5b89b04ba730d4cd249f811e8b48aa7d8c" } diff --git a/assets/tiles.png b/assets/tiles.png Binary files differnew file mode 100644 index 0000000..49d88b5 --- /dev/null +++ b/assets/tiles.png diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8644542 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,239 @@ +#![feature(try_blocks)] +use std::time::Duration; + +use bevy::{ + DefaultPlugins, + app::App, + camera::ScalingMode, + ecs::{event::Trigger, observer::ObservedBy, resource::IsResource}, + input::{common_conditions::input_just_pressed, keyboard::KeyboardInput}, + prelude::*, + window::{Window, WindowPlugin}, +}; +use bevy_ecs_tilemap::{helpers::square_grid::neighbors::Neighbors, prelude::*}; +use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin}; +use rand::{rng, seq::IteratorRandom}; + +use crate::picking::TilemapPickingPlugin; + +const W: usize = 20; +const H: usize = 10; +const BOMB_COUNT: usize = 30; +fn main() -> AppExit { + App::new() + .add_plugins( + DefaultPlugins + .set(WindowPlugin { + primary_window: Some(Window { + title: "Minesweeper!".into(), + ..default() + }), + ..default() + }) + .set(ImagePlugin::default_linear()), + ) + .insert_resource(Grid(ge())) + .add_plugins(TilemapPlugin) + // .add_plugins(EguiPlugin::default()) + // .add_plugins(WorldInspectorPlugin::new()) + .add_plugins(TilemapPickingPlugin) + .add_systems(Startup, startup) + // .add_systems(Update, movement) + .add_observer(|_: On<End>, q: Query<&mut TileTextureIndex, With<Bomb>>| { + for mut lem in q { + if lem.0 == 1 { + return; + } + if lem.0 != 12 { + lem.0 = 11; + } + } + }) + .add_observer( + |_: On<End>, q: Query<&mut TileTextureIndex, (Without<Bomb>, With<Flag>)>| { + for mut lem in q { + lem.0 = 13; + } + }, + ) + .add_observer( + |_: On<End>, q: Query<Entity, (With<TilePos>, With<ObservedBy>)>, mut c: Commands| { + // println!("{q:?}"); + for lem in q { + c.entity(lem).remove::<ObservedBy>(); + } + }, + ) + .add_systems(Update, reload.run_if(input_just_pressed(KeyCode::KeyR))) + .run() +} +#[derive(Resource)] +struct Grid([[u8; W]; W]); +#[derive(EntityEvent, Debug, Clone, Reflect)] +struct Zero(Entity); +#[derive(Event, Debug, Clone, Reflect)] +struct End; +#[derive(Component)] +struct Bomb; +#[derive(Component)] +struct Flag; +#[derive(Component)] +struct Murderable; + +macro_rules! all { + () => { + (0..W).flat_map(|x| (0..H).map(move |y| (x, y))) + }; +} + +fn reveal<E: EntityEvent>() +-> impl Fn(On<E>, Query<(&TilePos, &mut TileTextureIndex)>, Res<Grid>, Query<&TileStorage>, Commands) +{ + move |ev, mut tile, ge, store, mut cmds| { + let Ok((&tp @ TilePos { x, y }, mut sp)) = tile.get_mut(ev.event_target()) else { + return; + }; + *sp = TileTextureIndex(match ge.0[y as usize][x as usize] { + 0 if (sp.0 != 10 && sp.0 == 0) => { + let map_size = TilemapSize { + x: W as u32, + y: H as u32, + }; + for lem in Neighbors::get_square_neighboring_positions(&tp, &map_size, false) + .entities(store.single().unwrap()) + .iter() + { + cmds.trigger(Zero(*lem)); + } + 10 + } + n @ 1..=8 => 1 + n as u32, + _ => return, + }); + } +} +fn clicked() +-> impl Fn(On<Pointer<Press>>, Query<(&TilePos, &mut TileTextureIndex)>, Res<Grid>, Commands) { + move |ev, mut tile, ge, mut cmds| { + let Ok((&TilePos { x, y }, mut sp)) = tile.get_mut(ev.event_target()) else { + return; + }; + let real = ge.0[y as usize][x as usize]; + sp.0 = match ev.event().button { + PointerButton::Primary if sp.0 == 0 => match real { + 255 => { + cmds.trigger(End); + 12 + } + 0 => { + cmds.trigger(Zero(ev.event_target())); + return; + } + n @ 1..=8 => 1 + n as u32, + _ => 10, + }, + PointerButton::Secondary if sp.0 < 2 => { + if sp.0 == 0 { + cmds.entity(ev.event_target()).insert(Flag); + } else { + cmds.entity(ev.event_target()).remove::<Flag>(); + } + (!(sp.0 != 0)) as u32 + } + _ => return, + }; + } +} +fn reload( + mut commands: Commands, + mut gr: ResMut<Grid>, + asset_server: Res<AssetServer>, + q: Query<Entity, With<Murderable>>, +) { + gr.0 = ge(); + for lem in q { + commands.entity(lem).despawn(); + } + startup(commands, gr.into(), asset_server); +} +fn startup(mut commands: Commands, ge: Res<Grid>, asset_server: Res<AssetServer>) { + // commands.spawn(Camera2d); + commands.spawn(( + Camera2d, + Projection::Orthographic(OrthographicProjection { + scaling_mode: ScalingMode::AutoMin { + min_width: (W as f32 + 4.) * 256., + min_height: (H as f32 + 4.) * 256., + }, + ..OrthographicProjection::default_2d() + }), + Murderable, + )); + let texture_handle: Handle<Image> = asset_server.load("tiles.png"); + + let map_size = TilemapSize { + x: W as u32, + y: H as u32, + }; + + let mut tile_storage = TileStorage::empty(map_size); + let tilemap_entity = commands.spawn_empty().insert(Murderable).id(); + let tilemap_id = TilemapId(tilemap_entity); + + for (x, y) in all!() { + let position = TilePos { + x: x as u32, + y: y as u32, + }; + let mut e = commands.spawn(( + TileBundle { + texture_index: TileTextureIndex(0), + position, + tilemap_id, + ..default() + }, + Murderable, + )); + if ge.0[y][x] == 255 { + e.insert(Bomb); + } + let tile_entity = e.observe(clicked()).observe(reveal::<Zero>()).id(); + tile_storage.set(&position, tile_entity); + } + + let tile_size = TilemapTileSize { x: 256.0, y: 256.0 }; + let grid_size = tile_size.into(); + + commands.entity(tilemap_entity).insert(( + TilemapBundle { + grid_size, + size: map_size, + storage: tile_storage, + texture: TilemapTexture::Single(texture_handle), + tile_size, + map_type: TilemapType::Square, + anchor: TilemapAnchor::Center, + + ..Default::default() + }, + Murderable, + )); +} + +fn ge() -> [[u8; W]; W] { + let mut g = [[0u8; W]; W]; + + for (x, y) in all!().sample(&mut rng(), BOMB_COUNT) { + g[y][x] = 255; + let lems = aoc::util::nb((x, y)); + lems.into_iter().for_each(|(x, y)| { + try { + let lem = g.get_mut(y)?.get_mut(x)?; + *lem = lem.saturating_add(1); + }; + }); + } + + g +} +mod picking; diff --git a/src/picking.rs b/src/picking.rs new file mode 100644 index 0000000..282769c --- /dev/null +++ b/src/picking.rs @@ -0,0 +1,106 @@ +use bevy::camera::RenderTarget; +use bevy::picking::PickingSystems; +use bevy::picking::backend::HitData; +use bevy::picking::backend::PointerHits; +use bevy::picking::pointer::PointerId; +use bevy::picking::pointer::PointerLocation; +use bevy::prelude::*; +use bevy::window::PrimaryWindow; +use bevy_ecs_tilemap::prelude::*; + +pub struct TilemapPickingPlugin; + +impl Plugin for TilemapPickingPlugin { + fn build(&self, app: &mut App) { + app.add_systems(PreUpdate, tile_picking.in_set(PickingSystems::Backend)); + } +} + +fn tile_picking( + pointers_q: Query<(&PointerId, &PointerLocation)>, + cameras_q: Query<( + Entity, + &Camera, + &RenderTarget, + &GlobalTransform, + &Projection, + )>, + primary_window_q: Query<Entity, With<PrimaryWindow>>, + tilemap_q: Query<( + &TilemapSize, + &TilemapGridSize, + &TilemapTileSize, + &TilemapAnchor, + &TilemapType, + &TileStorage, + &GlobalTransform, + &ViewVisibility, + )>, + mut output: MessageWriter<PointerHits>, +) { + for (p_id, p_loc) in pointers_q + .iter() + .filter_map(|(p_id, p_loc)| p_loc.location().map(|l| (p_id, l))) + { + let Some((cam_entity, camera, _cam_target, cam_transform, _cam_ortho)) = cameras_q + .iter() + .filter(|(_, camera, ..)| camera.is_active) + .find(|(_, _, cam_target, ..)| { + cam_target + .normalize(match primary_window_q.single() { + Ok(w) => Some(w), + Err(_) => return false, + }) + .as_ref() + == Some(&p_loc.target) + }) + else { + continue; + }; + + let Ok(cursor_pos_world) = camera.viewport_to_world_2d(cam_transform, p_loc.position) + else { + continue; + }; + + let picks = tilemap_q + .iter() + .filter(|(.., vis)| vis.get()) + .filter_map( + |( + map_s, + map_grid_size, + map_tile_size, + map_anchor, + map_type, + map_store, + map_transform, + .., + )| { + let in_map_pos = { + let pos = Vec4::from((cursor_pos_world, 0., 1.0)); + let in_map_pos = map_transform.to_matrix().inverse() * pos; + in_map_pos.xy() + }; + + let tile_entity = TilePos::from_world_pos( + &in_map_pos, + map_s, + map_grid_size, + map_tile_size, + map_type, + map_anchor, + ) + .and_then(|tile_pos| map_store.get(&tile_pos))?; + + Some(( + tile_entity, + HitData::new(cam_entity, -map_transform.translation().z, None, None), + )) + }, + ) + .collect::<Vec<_>>(); + + output.write(PointerHits::new(*p_id, picks, camera.order as f32)); + } +} |