diff --git a/src/camera.rs b/src/camera.rs index 67ff703..845b463 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -5,10 +5,19 @@ use crate::{ game::{GRID_SIZE, HALF_TILE_SIZE, TILE_SIZE}, }; -pub const BACKGROUND_LAYER: u8 = 0; -pub const GROUND_LAYER: u8 = 1; -pub const SKY_LAYER: u8 = 2; -pub const UI_LAYER: u8 = 3; +pub enum RenderLayer { + Background = 0, + Topography, + Ground, + Sky, + Ui, +} + +impl From for u8 { + fn from(value: RenderLayer) -> Self { + value as u8 + } +} pub struct CameraPlugin; @@ -25,6 +34,7 @@ impl Plugin for CameraPlugin { ) .chain(), y_sorting, + inverse_y_sorting, ), ); } @@ -83,13 +93,19 @@ pub struct MainCamera; #[derive(Component)] pub struct YSorted; +#[derive(Component)] +pub struct YSortedInverse; + fn setup_camera(mut commands: Commands) { commands - .spawn(MainCameraBundle::from_layer(BACKGROUND_LAYER)) + .spawn(MainCameraBundle::from_layer(RenderLayer::Background.into())) .with_children(|builder| { - builder.spawn(SubLayerCameraBundle::from_layer(GROUND_LAYER)); - builder.spawn(SubLayerCameraBundle::from_layer(SKY_LAYER)); - builder.spawn(SubLayerCameraBundle::from_layer(UI_LAYER)); + builder.spawn(SubLayerCameraBundle::from_layer(RenderLayer::Ground.into())); + builder.spawn(SubLayerCameraBundle::from_layer( + RenderLayer::Topography.into(), + )); + builder.spawn(SubLayerCameraBundle::from_layer(RenderLayer::Sky.into())); + builder.spawn(SubLayerCameraBundle::from_layer(RenderLayer::Ui.into())); }); } @@ -143,3 +159,11 @@ pub fn y_sorting(mut query: Query<&mut Transform, (Changed, With, With)>, +) { + for mut transform in &mut query { + transform.translation.z = -transform.translation.normalize().y; + } +} diff --git a/src/game/combat.rs b/src/game/combat.rs index 7d94a18..18e8cc1 100644 --- a/src/game/combat.rs +++ b/src/game/combat.rs @@ -2,7 +2,7 @@ use bevy::{prelude::*, render::view::RenderLayers}; use bevy_rapier2d::prelude::*; use crate::{ - camera::{YSorted, SKY_LAYER}, + camera::{RenderLayer, YSorted}, playing, }; @@ -127,7 +127,7 @@ fn spawn_projectiles( damage: ImpactDamage(damage), emitter: Emitter(emitter), marker: Projectile, - render_layers: RenderLayers::layer(SKY_LAYER), + render_layers: RenderLayers::layer(RenderLayer::Sky.into()), rigid_body: RigidBody::Dynamic, sprite: SpriteBundle { sprite: Sprite { @@ -148,7 +148,7 @@ fn spawn_projectiles( projectile_entity_commands.insert(( Damping { linear_damping: 1.0, - ..default() + angular_damping: 10.0, }, InGameEntity, YSorted, diff --git a/src/game/enemy.rs b/src/game/enemy.rs index c77d5a0..501de29 100644 --- a/src/game/enemy.rs +++ b/src/game/enemy.rs @@ -3,7 +3,7 @@ use bevy_rapier2d::prelude::*; use rand::seq::IteratorRandom; use crate::{ - camera::{YSorted, GROUND_LAYER}, + camera::{RenderLayer, YSorted}, physics::Speed, playing, }; @@ -91,7 +91,7 @@ fn spawn_enemies( ..default() }, collider: Collider::cuboid(HALF_TILE_SIZE.x, HALF_TILE_SIZE.y), - render_layers: RenderLayers::layer(GROUND_LAYER), + render_layers: RenderLayers::layer(RenderLayer::Ground.into()), rigid_body: RigidBody::Dynamic, collision_groups: CollisionGroups::new( ENEMY_GROUP, diff --git a/src/game/fire_breath.rs b/src/game/fire_breath.rs index fb1bd74..55aab92 100644 --- a/src/game/fire_breath.rs +++ b/src/game/fire_breath.rs @@ -3,7 +3,7 @@ use bevy_particle_systems::*; use bevy_rapier2d::prelude::{Collider, CollisionGroups, Sensor}; use crate::{ - camera::{YSorted, SKY_LAYER}, + camera::{RenderLayer, YSorted}, playing, }; @@ -85,7 +85,7 @@ fn spawn_fire_breath( }, ..ParticleSystemBundle::default() }, - render_layers: RenderLayers::layer(SKY_LAYER), + render_layers: RenderLayers::layer(RenderLayer::Sky.into()), sensor: Sensor, collider: Collider::ball(25.0), damage: ImpactDamage(damage), diff --git a/src/game/level.rs b/src/game/level.rs index aef1ad7..3dde765 100644 --- a/src/game/level.rs +++ b/src/game/level.rs @@ -3,15 +3,16 @@ use bevy::{ ecs::system::SystemParam, prelude::*, render::view::RenderLayers, + sprite::Anchor, }; use bevy_rapier2d::prelude::*; use noise::{NoiseFn, Perlin}; use pathfinding::prelude::Matrix; -use rand::{random, seq::SliceRandom}; +use rand::{random, seq::SliceRandom, Rng}; use crate::{ audio::{PlayMusicEvent, SoundEffect}, - camera::{YSorted, BACKGROUND_LAYER, GROUND_LAYER}, + camera::{RenderLayer, YSorted, YSortedInverse}, entity_cleanup, game::{ InGameEntity, BUILDING_GROUP, ENEMY_GROUP, FIRE_BREATH_GROUP, GRID_SIZE, HALF_GRID_SIZE, @@ -35,12 +36,19 @@ impl Plugin for LevelPlugin { from: AppState::MainMenu, to: AppState::InGame, }, - generate_level_matrix, + (generate_level_matrix, generate_tilemaps), ); app.add_systems( OnEnter(AppState::InGame), - (spawn_level_tiles, spawn_buildings, play_background_music).chain(), + ( + spawn_level_tiles, + spawn_buildings, + spawn_hills, + spawn_mountains, + play_background_music, + ) + .chain(), ); app.add_systems( @@ -50,6 +58,26 @@ impl Plugin for LevelPlugin { } } +fn generate_tilemaps(mut commands: Commands, asset_server: Res) { + let tileset_ground_texture = asset_server + .get_handle("textures/tileset_ground.png") + .unwrap_or_default(); + let tileset_objects_texture = asset_server + .get_handle("textures/tileset_objects.png") + .unwrap_or_default(); + let tileset_ground_texture_atlas = + TextureAtlas::from_grid(tileset_ground_texture, TILE_SIZE, 16, 18, None, None); + let tileset_objects_texture_atlas = + TextureAtlas::from_grid(tileset_objects_texture, TILE_SIZE, 38, 14, None, None); + + commands.insert_resource(TilesetGroundTextureAtlasHandle( + asset_server.add(tileset_ground_texture_atlas), + )); + commands.insert_resource(TilesetObjectsTextureAtlasHandle( + asset_server.add(tileset_objects_texture_atlas), + )); +} + fn generate_level_matrix(mut commands: Commands) { const MAP_OFFSET_X: f64 = 0.; const MAP_OFFSET_Y: f64 = 0.; @@ -80,20 +108,21 @@ fn generate_level_matrix(mut commands: Commands) { commands.insert_resource(LevelMatrix(level_matrix)); } -fn spawn_level_tiles(mut commands: Commands, level_matrix: Res) { +fn spawn_level_tiles( + mut commands: Commands, + level_matrix: Res, + tileset_ground_texture_atlas_handle: Res, +) { for ((x, y), tile) in level_matrix.0.items() { let tile = *tile; let position = translate_grid_position_to_world_space(&(x, y)); let translation = position.extend(0.0); let transform = Transform::from_translation(translation); let mut tile_entity = commands.spawn(TileBundle { - render_layers: RenderLayers::layer(BACKGROUND_LAYER), - sprite: SpriteBundle { - sprite: Sprite { - color: tile.into(), - custom_size: Some(TILE_SIZE), - ..default() - }, + render_layers: RenderLayers::layer(RenderLayer::Background.into()), + sprite: SpriteSheetBundle { + sprite: TextureAtlasSprite::new(tile.into()), + texture_atlas: tileset_ground_texture_atlas_handle.clone(), transform, ..default() }, @@ -108,7 +137,11 @@ fn spawn_level_tiles(mut commands: Commands, level_matrix: Res) { } } -fn spawn_buildings(mut commands: Commands, level_matrix: Res) { +fn spawn_buildings( + mut commands: Commands, + level_matrix: Res, + asset_server: Res, +) { const BUILDING_SPAWN_CHANCE: f32 = 0.01; let grass_tiles: Vec = level_matrix @@ -116,39 +149,148 @@ fn spawn_buildings(mut commands: Commands, level_matrix: Res) { .filter(|(_, tile)| **tile == Tile::Grass) .map(|(pos, _)| translate_grid_position_to_world_space(&pos)) .collect(); - let total_buildings = (GRID_SIZE.x * GRID_SIZE.y * BUILDING_SPAWN_CHANCE).ceil() as u32; + let total_buildings = (grass_tiles.len() as f32 * BUILDING_SPAWN_CHANCE).ceil() as usize; let mut rng = rand::thread_rng(); + let random_spawn_points = grass_tiles.choose_multiple(&mut rng, total_buildings); + let building_tile_variants = [ + Rect::from_corners(Vec2::new(352., 96.), Vec2::new(400., 144.)), + Rect::from_corners(Vec2::new(400., 96.), Vec2::new(448., 144.)), + ]; + let texture = asset_server + .get_handle("textures/tileset_objects.png") + .unwrap_or_default(); - for _ in 0..total_buildings { - if let Some(position) = grass_tiles.choose(&mut rng) { - let translation = position.extend(1.); - let mut building_entity_commands = commands.spawn(BuildingBundle { - active_collision_types: ActiveCollisionTypes::all(), - attack_damage: AttackDamage(5), - attack_timer: AttackTimer::new(6.), - collider: Collider::ball(HALF_TILE_SIZE.x), - collision_groups: CollisionGroups::new( - BUILDING_GROUP, - ENEMY_GROUP | FIRE_BREATH_GROUP, - ), - hitpoints: ResourcePool::::new(1000), - marker: Enemy, - range: Range(TILE_SIZE.x * 15.), - render_layers: RenderLayers::layer(GROUND_LAYER), - rigid_body: RigidBody::Fixed, - sprite: SpriteBundle { - sprite: Sprite { - color: Color::DARK_GRAY, - custom_size: Some(TILE_SIZE), - ..default() - }, - transform: Transform::from_translation(translation), + for position in random_spawn_points { + let translation = position.extend(1.); + let mut building_entity_commands = commands.spawn(BuildingBundle { + active_collision_types: ActiveCollisionTypes::all(), + attack_damage: AttackDamage(5), + attack_timer: AttackTimer::new(6.), + collider: Collider::ball(HALF_TILE_SIZE.x), + collision_groups: CollisionGroups::new(BUILDING_GROUP, ENEMY_GROUP | FIRE_BREATH_GROUP), + hitpoints: ResourcePool::::new(1000), + marker: Enemy, + range: Range(TILE_SIZE.x * 15.), + render_layers: RenderLayers::layer(RenderLayer::Ground.into()), + rigid_body: RigidBody::Fixed, + sprite: SpriteBundle { + sprite: Sprite { + flip_x: rng.gen_bool(0.5), + rect: Some(*building_tile_variants.choose(&mut rng).unwrap()), ..default() }, - }); + texture: texture.clone(), + transform: Transform::from_translation(translation), + ..default() + }, + }); - building_entity_commands.insert((InGameEntity, YSorted)); - } + building_entity_commands.insert((InGameEntity, YSorted)); + } +} + +fn spawn_hills( + mut commands: Commands, + level_matrix: Res, + asset_server: Res, +) { + const POSITION_OFFSET_FACTOR: f32 = 15.; + + let hill_tiles: Vec = level_matrix + .items() + .filter(|(_, tile)| **tile == Tile::Hills) + .map(|(pos, _)| translate_grid_position_to_world_space(&pos)) + .collect(); + let mut rng = rand::thread_rng(); + let hill_tile_variants = [ + Rect::from_corners(Vec2::new(320., 64.), Vec2::new(368., 96.)), + Rect::from_corners(Vec2::new(368., 64.), Vec2::new(400., 96.)), + Rect::from_corners(Vec2::new(400., 80.), Vec2::new(432., 96.)), + Rect::from_corners(Vec2::new(432., 80.), Vec2::new(448., 96.)), + ]; + let texture = asset_server + .get_handle("textures/tileset_objects.png") + .unwrap_or_default(); + + for position in hill_tiles { + let position_offset = Vec2::new( + rng.gen::() * POSITION_OFFSET_FACTOR, + -HALF_TILE_SIZE.y + rng.gen::() * POSITION_OFFSET_FACTOR, + ); + let translation = (position + position_offset).extend(1.); + let mut hill_entity_commands = commands.spawn(SpriteBundle { + sprite: Sprite { + anchor: bevy::sprite::Anchor::BottomCenter, + flip_x: rng.gen_bool(0.2), + rect: Some(*hill_tile_variants.choose(&mut rng).unwrap()), + ..default() + }, + texture: texture.clone(), + transform: Transform::from_translation(translation), + ..default() + }); + + hill_entity_commands.insert(( + RenderLayers::layer(RenderLayer::Topography.into()), + InGameEntity, + YSorted, + )); + } +} + +fn spawn_mountains( + mut commands: Commands, + asset_server: Res, + level_matrix: Res, +) { + const MOUNTAIN_TILE_SIZE: Vec2 = Vec2::new(64., 48.); + const POSITION_OFFSET_FACTOR: f32 = 20.; + + let mountain_tiles: Vec = level_matrix + .items() + .filter(|(_, tile)| **tile == Tile::Mountains) + .map(|(pos, _)| translate_grid_position_to_world_space(&pos)) + .collect(); + let mut rng = rand::thread_rng(); + let mountain_tile_variants = [ + Rect::from_corners(Vec2::ZERO, MOUNTAIN_TILE_SIZE), + Rect::from_corners( + Vec2::X * MOUNTAIN_TILE_SIZE.x, + MOUNTAIN_TILE_SIZE + (Vec2::X * MOUNTAIN_TILE_SIZE.x), + ), + Rect::from_corners( + Vec2::Y * MOUNTAIN_TILE_SIZE.y, + MOUNTAIN_TILE_SIZE + (Vec2::Y * MOUNTAIN_TILE_SIZE.y), + ), + Rect::from_corners(MOUNTAIN_TILE_SIZE, MOUNTAIN_TILE_SIZE * 2.), + ]; + let texture = asset_server + .get_handle("textures/tileset_objects.png") + .unwrap_or_default(); + + for position in mountain_tiles { + let position_offset = Vec2::new( + rng.gen::() * POSITION_OFFSET_FACTOR, + -MOUNTAIN_TILE_SIZE.y / 2. + rng.gen::() * POSITION_OFFSET_FACTOR, + ); + let translation = (position + position_offset).extend(1.); + let mut mountain_entity_commands = commands.spawn(SpriteBundle { + sprite: Sprite { + anchor: Anchor::BottomCenter, + flip_x: rng.gen_bool(0.3), + rect: Some(*mountain_tile_variants.choose(&mut rng).unwrap()), + ..default() + }, + texture: texture.clone(), + transform: Transform::from_translation(translation), + ..default() + }); + + mountain_entity_commands.insert(( + RenderLayers::layer(RenderLayer::Topography.into()), + InGameEntity, + YSortedInverse, + )); } } @@ -163,6 +305,12 @@ fn play_background_music(mut play_music_event_writer: EventWriter); + +#[derive(Resource, Deref)] +pub struct TilesetObjectsTextureAtlasHandle(Handle); + #[derive(Resource, Deref)] pub struct LevelMatrix(Matrix); @@ -187,7 +335,7 @@ pub struct BorderTile; #[derive(Bundle)] pub struct TileBundle { pub render_layers: RenderLayers, - pub sprite: SpriteBundle, + pub sprite: SpriteSheetBundle, pub tile: Tile, } @@ -231,6 +379,26 @@ impl From for Color { } } +impl From for usize { + fn from(value: Tile) -> Self { + match value { + Tile::Grass => 34, + Tile::Hills => 242, + Tile::Mountains => 242, + Tile::Sand => 183, + Tile::Water => { + if random::() > 0.1 { + 145 + } else { + let mut rng = rand::thread_rng(); + *[146_usize, 147, 148].choose(&mut rng).unwrap() + } + } + Tile::_LAST => 0, + } + } +} + #[derive(SystemParam)] pub struct TileQuery<'w, 's> { tile_query: Query<'w, 's, (&'static Tile, &'static Transform)>, diff --git a/src/game/player.rs b/src/game/player.rs index a9aa68b..07bad9e 100644 --- a/src/game/player.rs +++ b/src/game/player.rs @@ -3,7 +3,7 @@ use bevy_rapier2d::prelude::*; use crate::{ animation::{AnimationIndices, AnimationTimer}, - camera::{YSorted, SKY_LAYER}, + camera::{RenderLayer, YSorted}, AppState, }; @@ -54,7 +54,7 @@ fn spawn_player(mut commands: Commands, asset_server: Res) { hitpoints: ResourcePool::::new(100), score: Score::new(0, 1), marker: Player, - render_layers: RenderLayers::layer(SKY_LAYER), + render_layers: RenderLayers::layer(RenderLayer::Sky.into()), spritesheet: SpriteSheetBundle { sprite: TextureAtlasSprite::new(0), texture_atlas: texture_atlas_handle.clone(),