diff --git a/Cargo.lock b/Cargo.lock index 480ce20..d754b4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -885,6 +885,7 @@ dependencies = [ "hexx", "image", "rand", + "rand_chacha", "winit", ] diff --git a/Cargo.toml b/Cargo.toml index 259cdee..cc11064 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "bevy_game" # ToDo +name = "bevy_game" # ToDo version = "0.1.0" publish = false -authors = ["Niklas Eicker "] # ToDo: you are the author ;) +authors = ["Niklas Eicker "] # ToDo: you are the author ;) edition = "2021" exclude = ["dist", "build", "assets", "credits"] @@ -22,14 +22,12 @@ inherits = "release" lto = "thin" [features] -dev = [ - "bevy/dynamic_linking", -] +dev = ["bevy/dynamic_linking"] # All of Bevy's default features exept for the audio related ones, since they clash with bevy_kira_audio # and android_shared_stdcxx, since that is covered in `mobile` [dependencies] -bevy = { version="0.12", default-features = false, features = [ +bevy = { version = "0.12", default-features = false, features = [ "animation", "bevy_asset", "bevy_gilrs", @@ -56,6 +54,7 @@ bevy = { version="0.12", default-features = false, features = [ bevy_kira_audio = { version = "0.18" } bevy_asset_loader = { version = "0.18" } rand = { version = "0.8.3" } +rand_chacha = "0.3" # keep the following in sync with Bevy's dependencies winit = { version = "0.28", default-features = false } diff --git a/src/actions/game_control.rs b/src/actions/game_control.rs index a10d87d..85a9c06 100644 --- a/src/actions/game_control.rs +++ b/src/actions/game_control.rs @@ -1,5 +1,6 @@ use bevy::prelude::{Input, KeyCode, Res}; +#[allow(dead_code)] pub enum GameControl { Up, Down, diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 5e58ae7..f5a7dd8 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -3,8 +3,6 @@ use bevy::prelude::*; pub mod cursor; mod game_control; -pub const FOLLOW_EPSILON: f32 = 5.; - pub struct ActionsPlugin; // This plugin listens for keyboard input and converts the input into Actions diff --git a/src/audio.rs b/src/audio.rs index 8a58e17..e2590ea 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -33,7 +33,7 @@ fn start_audio(mut commands: Commands, audio_assets: Res, audio: Re commands.insert_resource(FlyingAudio(handle)); } -fn control_flying_sound( +fn _control_flying_sound( actions: Res, audio: Res, mut audio_instances: ResMut>, diff --git a/src/buildings.rs b/src/buildings.rs new file mode 100644 index 0000000..9e1c408 --- /dev/null +++ b/src/buildings.rs @@ -0,0 +1,296 @@ +use crate::inventory::{self}; +use crate::inventory::{Inventory, SpawnInventory}; +use crate::random::RandomDeterministic; +use crate::window::WindowSize; +use crate::GameState; +use bevy::ecs::system::{EntityCommand, SystemParam, SystemState}; + +use bevy::prelude::*; +use bevy::render::mesh::Indices; +use bevy::render::render_resource::PrimitiveTopology; +use bevy::sprite::MaterialMesh2dBundle; +use bevy::sprite::Mesh2dHandle; +use bevy::utils::HashMap; +use rand::seq::SliceRandom; + +pub struct Plugin; + +impl bevy::prelude::Plugin for Plugin { + fn build(&self, app: &mut App) { + app.add_plugins(inventory::InventoryPlugin::::default()) + .init_resource::() + .add_systems( + OnEnter(GameState::Playing), + (create_assets, spawn_layout).chain(), + ) + .add_systems( + Update, + update_anchor_position + .run_if(resource_changed::()) + .run_if(in_state(GameState::Playing)), + ); + } +} + +#[derive(Resource)] +pub struct BuildingInventory { + pub(crate) state: SystemState>, +} + +#[derive(SystemParam)] +pub(crate) struct GetNextBuildingParams<'w, 's> { + command: Commands<'w, 's>, + q_inventory: Query< + 'w, + 's, + ( + &'static mut RandomDeterministic, + &'static mut crate::inventory::Inventory, + ), + >, + q_buildings: Query<'w, 's, &'static Building>, +} + +impl FromWorld for BuildingInventory { + fn from_world(world: &mut World) -> Self { + BuildingInventory { + state: SystemState::new(world), + } + } +} + +impl BuildingInventory { + pub fn next(&mut self, world: &mut World) -> Option { + let mut params = self.state.get_mut(world); + let (mut rng, mut inventory) = params.q_inventory.single_mut(); + + let Some(first_item) = inventory.items.front().cloned() else { + return None; + }; + let Ok(_item_to_build) = params.q_buildings.get(first_item) else { + return None; + }; + // TODO: check if we can build item_to_build (cooldown, space available, currency, ...) + // TODO: send an event if not possible. + // TODO: pay "price" ? + inventory.items.pop_front(); + + let new_building = get_random_building(&mut rng); + let new_item = params.command.spawn(new_building).id(); + + inventory.items.push_back(new_item); + + // TODO: reuse that entity to merge it with turret entity ? + world.despawn(first_item); + + self.state.apply(world); + Some(new_building) + } +} + +#[derive(Resource)] +pub struct VisualAssets { + pub mesh_def: HashMap, + pub size_def: HashMap, + pub color_def: HashMap>, +} + +pub(crate) fn create_assets( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.insert_resource(VisualAssets { + mesh_def: [ + ( + BuildingMesh::Triangle, + meshes + .add( + Mesh::new(PrimitiveTopology::TriangleList) + .with_inserted_attribute( + Mesh::ATTRIBUTE_POSITION, + vec![[-0.5, -0.5, 0.0], [0.0, 0.5, 0.0], [0.5, -0.5, 0.0]], + ) + .with_indices(Some(Indices::U32(vec![0, 1, 2]))), + ) + .into(), + ), + ( + BuildingMesh::Circle, + meshes.add(Mesh::from(shape::Circle::default())).into(), + ), + ( + BuildingMesh::Quad, + meshes.add(Mesh::from(shape::Quad::default())).into(), + ), + ] + .into(), + size_def: [ + (BuildingSize::Big, 1f32), + (BuildingSize::Medium, 0.75f32), + (BuildingSize::Small, 0.5f32), + ] + .into(), + color_def: [ + ( + BuildingColor::Black, + materials.add(ColorMaterial::from(Color::BLACK)), + ), + ( + BuildingColor::White, + materials.add(ColorMaterial::from(Color::WHITE)), + ), + ( + BuildingColor::Pink, + materials.add(ColorMaterial::from(Color::PINK)), + ), + ( + BuildingColor::Blue, + materials.add(ColorMaterial::from(Color::BLUE)), + ), + ] + .into(), + }); +} + +const ITEM_VISUAL_SIZE: f32 = 64f32; +const PADDING: f32 = 10f32; + +pub(crate) fn spawn_layout(mut commands: Commands, window_size: ResMut) { + let mut rng = crate::random::RandomDeterministic::new_from_seed(0); + let inventory = vec![ + commands.spawn(get_random_building(&mut rng)).id(), + commands.spawn(get_random_building(&mut rng)).id(), + commands.spawn(get_random_building(&mut rng)).id(), + commands.spawn(get_random_building(&mut rng)).id(), + commands.spawn(get_random_building(&mut rng)).id(), + commands.spawn(get_random_building(&mut rng)).id(), + ]; + let anchor_point = Vec3::new( + -window_size.size.x / 2f32 + ITEM_VISUAL_SIZE / 2f32 + PADDING, + -window_size.size.y / 2f32 + (ITEM_VISUAL_SIZE + PADDING) * 5.5f32 + PADDING, + 0f32, + ); + + commands + .spawn_empty() + .add(SpawnInventory::::new( + inventory, + inventory::InventoryConfiguration { + positions: positions_from_anchor_point(anchor_point), + }, + )) + .insert(RandomDeterministic::new_from_seed(0)); +} + +fn positions_from_anchor_point(anchor_point: Vec3) -> Vec { + vec![ + anchor_point - Vec3::new(0f32, (ITEM_VISUAL_SIZE + PADDING) * 5f32, 0f32), + anchor_point - Vec3::new(0f32, (ITEM_VISUAL_SIZE + PADDING) * 4f32, 0f32), + anchor_point - Vec3::new(0f32, (ITEM_VISUAL_SIZE + PADDING) * 3f32, 0f32), + anchor_point - Vec3::new(0f32, (ITEM_VISUAL_SIZE + PADDING) * 2f32, 0f32), + anchor_point - Vec3::new(0f32, ITEM_VISUAL_SIZE + PADDING, 0f32), + anchor_point, + ] +} + +pub(crate) fn update_anchor_position( + window_size: ResMut, + mut q_inventory: Query<&mut Inventory>, +) { + let anchor_point: Vec3 = Vec3::new( + -window_size.size.x / 2f32 + ITEM_VISUAL_SIZE / 2f32 + PADDING, + -window_size.size.y / 2f32 + (ITEM_VISUAL_SIZE + PADDING) * 5.5f32 + PADDING, + 0f32, + ); + q_inventory.for_each_mut(|mut inventory| { + inventory.positions = positions_from_anchor_point(anchor_point); + }); +} + +#[derive(Component, Clone, Copy, Hash, Eq, PartialEq)] +pub struct Building { + mesh: BuildingMesh, + size: BuildingSize, + color: BuildingColor, +} + +#[derive(Clone, Copy, Hash, Eq, PartialEq)] +pub enum BuildingMesh { + Triangle, + Circle, + Quad, +} +#[derive(Clone, Copy, Hash, Eq, PartialEq)] +pub enum BuildingSize { + Small, + Medium, + Big, +} +#[derive(Clone, Copy, Hash, Eq, PartialEq)] +pub enum BuildingColor { + Black, + White, + Pink, + Blue, +} + +impl inventory::ItemSpriteBuilder for Building { + type C = BuildingItemSpriteBuilder; + fn build_sprite(&self) -> Self::C { + BuildingItemSpriteBuilder { building: *self } + } +} + +pub struct BuildingItemSpriteBuilder { + pub building: Building, +} + +impl EntityCommand for BuildingItemSpriteBuilder { + fn apply(self, id: Entity, world: &mut World) { + let assets = world.get_resource::().unwrap(); + let visual = MaterialMesh2dBundle { + mesh: assets.mesh_def[&self.building.mesh].clone(), + transform: Transform::default().with_scale(Vec3::splat( + ITEM_VISUAL_SIZE * assets.size_def[&self.building.size], + )), + material: assets.color_def[&self.building.color].clone(), + ..default() + }; + world.entity_mut(id).insert(visual); + } +} + +pub fn get_random_building(rng: &mut crate::random::RandomDeterministic) -> Building { + let choices_mesh = [ + (BuildingMesh::Triangle, 2), + (BuildingMesh::Circle, 2), + (BuildingMesh::Quad, 2), + ]; + let choices_size = [ + (BuildingSize::Big, 1), + (BuildingSize::Medium, 2), + (BuildingSize::Small, 1), + ]; + let choices_color = [ + (BuildingColor::Black, 5), + (BuildingColor::White, 5), + (BuildingColor::Pink, 1), + (BuildingColor::Blue, 1), + ]; + let building = Building { + mesh: choices_mesh + .choose_weighted(&mut rng.random, |i| i.1) + .unwrap() + .0, + size: choices_size + .choose_weighted(&mut rng.random, |i| i.1) + .unwrap() + .0, + color: choices_color + .choose_weighted(&mut rng.random, |i| i.1) + .unwrap() + .0, + }; + building +} diff --git a/src/inventory.rs b/src/inventory.rs new file mode 100644 index 0000000..070d090 --- /dev/null +++ b/src/inventory.rs @@ -0,0 +1,123 @@ +use bevy::ecs::system::EntityCommand; +use bevy::prelude::*; +use std::collections::VecDeque; +use std::marker::PhantomData; + +/// This plugin handles the creation of Items in the inventory +pub struct InventoryPlugin { + _item_type: PhantomData, +} + +impl Default for InventoryPlugin { + fn default() -> Self { + Self { + _item_type: Default::default(), + } + } +} + +impl Plugin for InventoryPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + PostUpdate, + ( + item_create_sprite::, + apply_deferred, + redraw_inventory_on_change::, + ) + .chain(), + ); + } +} + +pub trait ItemSpriteBuilder { + type C: EntityCommand; + fn build_sprite(&self) -> Self::C; +} + +#[derive(Component)] +struct MarkerItemSpriteBuilt; + +#[derive(Component)] +pub struct Inventory { + /// entities contained here have a MarkerItem component, it handles logic + /// their rendering is created via item_create_visual + pub items: VecDeque, + pub positions: Vec, + + _item_type: PhantomData, +} + +pub struct SpawnInventory { + items: Vec, + configuration: InventoryConfiguration, + + _item_type: PhantomData, +} + +impl SpawnInventory +where + IT: Component + ItemSpriteBuilder, +{ + pub fn new(items: Vec, configuration: InventoryConfiguration) -> Self { + Self { + items, + configuration, + _item_type: Default::default(), + } + } +} + +/// Configuration for the inventory +/// positions: Vec - positions of the items in the inventory +/// TODO: should be relative to the inventory entity/transform +pub struct InventoryConfiguration { + pub positions: Vec, +} + +impl EntityCommand for SpawnInventory +where + IT: Component + ItemSpriteBuilder, +{ + fn apply(self, id: Entity, world: &mut World) { + world.entity_mut(id).insert((Inventory:: { + items: self.items.into_iter().collect(), + positions: self.configuration.positions, + _item_type: self._item_type, + },)); + } +} + +fn item_create_sprite( + mut commands: Commands, + inventory: Query<&Inventory, Changed>>, + items_without_visual: Query<(Entity, &IT), Without>, +) { + for inventory in inventory.iter() { + for item in inventory.items.iter().take(inventory.positions.len()) { + if let Ok((entity, item)) = items_without_visual.get(*item) { + let mut c = commands.entity(entity); + c.add(item.build_sprite()).insert(MarkerItemSpriteBuilt); + } + } + } +} + +fn redraw_inventory_on_change( + inventory: Query<&Inventory, Changed>>, + mut items_with_visual: Query<&mut Transform, (With, With)>, +) { + for inventory in inventory.iter() { + for (i, &item) in inventory + .items + .iter() + .take(inventory.positions.len()) + .enumerate() + { + if let Ok(mut transform) = items_with_visual.get_mut(item) { + //TODO: should be relative to the inventory entity/transform + transform.translation = inventory.positions[i]; + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 8cf80b3..7794e62 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,18 @@ mod actions; mod audio; mod board; +mod buildings; mod bullet; mod crystal; mod enemy; mod grid; +mod inventory; mod loading; mod menu; mod primitives; +mod random; mod turret; +mod window; use crate::actions::ActionsPlugin; use crate::audio::InternalAudioPlugin; @@ -25,6 +29,7 @@ use bullet::BulletPlugin; use crystal::CrystalPlugin; use grid::GridPlugin; use primitives::PrimitivesPlugin; +use window::GameWindowPlugin; #[derive(States, Default, Clone, Eq, PartialEq, Debug, Hash)] pub enum GameState { @@ -52,6 +57,7 @@ impl Plugin for GamePlugin { DefaultPickingPlugins, CrystalPlugin, PrimitivesPlugin, + GameWindowPlugin, )); #[cfg(debug_assertions)] diff --git a/src/primitives/view.rs b/src/primitives/view.rs index 51a5be9..6286cd9 100644 --- a/src/primitives/view.rs +++ b/src/primitives/view.rs @@ -140,7 +140,7 @@ pub fn auto_remove_target_when_out_of_range( } } -pub fn debug_range(mut gizmos: Gizmos, views: Query<(&View, &Transform)>) { +pub fn _debug_range(mut gizmos: Gizmos, views: Query<(&View, &Transform)>) { for (view, transform) in &views { gizmos.circle_2d(transform.translation.xy(), view.range, Color::LIME_GREEN); } diff --git a/src/random.rs b/src/random.rs new file mode 100644 index 0000000..f30e78d --- /dev/null +++ b/src/random.rs @@ -0,0 +1,32 @@ +use bevy::prelude::*; +use rand::prelude::*; +use rand_chacha::ChaCha20Rng; + +#[allow(dead_code)] +#[derive(Component)] +pub struct RandomDeterministic { + pub random: ChaCha20Rng, + seed: u64, +} + +impl Default for RandomDeterministic { + fn default() -> Self { + let seed = 0; //thread_rng().gen::(); + Self::new_from_seed(seed) + } +} + +impl RandomDeterministic { + pub fn new_from_seed(seed: u64) -> RandomDeterministic { + Self { + random: ChaCha20Rng::seed_from_u64(seed), + seed, + } + } + pub fn _reset(&mut self) { + *self = Self::new_from_seed(self.seed); + } + pub fn _get_seed(&self) -> u64 { + self.seed + } +} diff --git a/src/turret.rs b/src/turret.rs index 7fa59c7..52ac9c9 100644 --- a/src/turret.rs +++ b/src/turret.rs @@ -1,6 +1,7 @@ use std::{f32::consts::FRAC_PI_2, time::Duration}; use crate::{ + buildings::{self, BuildingInventory}, bullet::SpawnBullet, enemy::Enemy, grid::HexGrid, @@ -19,6 +20,7 @@ pub struct TurretPlugin; impl Plugin for TurretPlugin { fn build(&self, app: &mut App) { + app.add_plugins(buildings::Plugin); app.add_systems( Update, ( @@ -27,7 +29,7 @@ impl Plugin for TurretPlugin { process_enemy_enter_range, process_enemy_exit_range, animate_targeting, - //auto_fire, + auto_fire, ) .run_if(in_state(GameState::Playing)), ); @@ -60,6 +62,12 @@ pub struct SpawnTurret { impl EntityCommand for SpawnTurret { fn apply(self, id: Entity, world: &mut World) { + // TODO: attach building to the turret + let _building = + world.resource_scope(|world, mut building_inventory: Mut| { + building_inventory.next(world) + }); + let texture = world.resource_scope(|_, asset_server: Mut| { asset_server.load("textures/DifferentTurrets/Turret01.png") });