Skip to content

Commit

Permalink
Buildings (#87)
Browse files Browse the repository at this point in the history
Issue:
==============
Closes #16

What was done:
==============
* Added randomly placed destructible buildings that throw arrows to the
player.
* Despawn entities when their current health reaches zero.
* Tweaked the fire breath damage value so it takes a bit longer to
destroy buildings.
* Cleaned up sound effects when exiting the InGame state.
  • Loading branch information
mnmaita authored Nov 26, 2023
1 parent 8834b93 commit c590f99
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 27 deletions.
5 changes: 4 additions & 1 deletion src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ impl PlaySoundEffectEvent {
#[derive(Component)]
pub struct BackgroundMusic;

#[derive(Component)]
pub struct SoundEffect;

fn handle_play_music_events(
asset_server: Res<AssetServer>,
mut commands: Commands,
Expand Down Expand Up @@ -182,7 +185,7 @@ fn handle_play_sound_effect_events(
let settings = settings.unwrap_or(PlaybackSettings::DESPAWN);
let path = format!("{ASSET_FOLDER_SFX}/{file_name}");
let source = asset_server.get_handle(path).unwrap_or_default();
let mut entity = commands.spawn(AudioBundle { source, settings });
let mut entity = commands.spawn((AudioBundle { source, settings }, SoundEffect));

if let Some(transform) = spatial_transform {
entity.insert(SpatialBundle::from_transform(*transform));
Expand Down
41 changes: 30 additions & 11 deletions src/game/combat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use super::{
level::TileQuery,
resource_pool::{Fire, Health, ResourcePool},
score_system::{ScoreEvent, ScoreEventType},
Enemy, InGameEntity, Player, Tile, HALF_TILE_SIZE,
Enemy, InGameEntity, Player, Tile, HALF_TILE_SIZE, PLAYER_GROUP, PROJECTILE_GROUP,
};

pub(super) struct CombatPlugin;
Expand All @@ -25,6 +25,7 @@ impl Plugin for CombatPlugin {
projectile_collision_with_player,
spawn_projectiles,
despawn_projectiles,
despawn_dead_entities,
compute_damage_from_intersections,
)
.run_if(playing()),
Expand Down Expand Up @@ -82,7 +83,13 @@ pub struct ImpactDamage(pub i16);
pub struct AttackDamage(pub i16);

#[derive(Component, Deref, DerefMut)]
pub struct AttackTimer(pub Timer);
pub struct AttackTimer(Timer);

impl AttackTimer {
pub fn new(seconds: f32) -> Self {
Self(Timer::from_seconds(seconds, TimerMode::Repeating))
}
}

#[derive(Component)]
pub struct Projectile;
Expand Down Expand Up @@ -113,7 +120,10 @@ fn spawn_projectiles(
let mut projectile_entity_commands = commands.spawn(ProjectileBundle {
ccd: Ccd::enabled(),
collider: Collider::cuboid(size.x / 2., size.y / 2.),
collision_groups: CollisionGroups::new(Group::GROUP_3, Group::GROUP_1 | Group::GROUP_3),
collision_groups: CollisionGroups::new(
PROJECTILE_GROUP,
PLAYER_GROUP | PROJECTILE_GROUP,
),
damage: ImpactDamage(damage),
emitter: Emitter(emitter),
marker: Projectile,
Expand Down Expand Up @@ -148,8 +158,8 @@ fn spawn_projectiles(

fn projectile_collision_with_player(
mut commands: Commands,
mut player_query: Query<(Entity, &mut ResourcePool<Health>), With<Player>>,
mut score_event_writer: EventWriter<ScoreEvent>,
mut player_query: Query<(Entity, &mut ResourcePool<Health>), With<Player>>,
projectile_query: Query<(Entity, &ImpactDamage), With<Projectile>>,
rapier_context: Res<RapierContext>,
) {
Expand All @@ -169,28 +179,37 @@ fn projectile_collision_with_player(
}

fn compute_damage_from_intersections(
mut commands: Commands,
mut enemy_query: Query<&mut ResourcePool<Health>, With<Enemy>>,
fire_query: Query<(Entity, &ImpactDamage), With<Fire>>,
mut enemy_query: Query<(Entity, &mut ResourcePool<Health>), With<Enemy>>,
mut score_event_writer: EventWriter<ScoreEvent>,
rapier_context: Res<RapierContext>,
) {
for (entity, damage) in &fire_query {
for (entity1, entity2, intersecting) in rapier_context.intersections_with(entity) {
let other_entity = if entity1 == entity { entity2 } else { entity1 };

if intersecting {
if let Ok((enemy_entity, mut enemy_hitpoints)) = enemy_query.get_mut(other_entity) {
if let Ok(mut enemy_hitpoints) = enemy_query.get_mut(other_entity) {
enemy_hitpoints.subtract(damage.0);
commands.entity(enemy_entity).despawn_recursive();
score_event_writer.send(ScoreEvent::new(10, ScoreEventType::AddPoints));
}
}
}
}
}

pub fn despawn_projectiles(
fn despawn_dead_entities(
mut commands: Commands,
mut score_event_writer: EventWriter<ScoreEvent>,
query: Query<(Entity, &ResourcePool<Health>), (Without<Player>, Changed<ResourcePool<Health>>)>,
) {
for (entity, health) in &query {
if health.current() == 0 {
commands.entity(entity).despawn_recursive();
score_event_writer.send(ScoreEvent::new(10, ScoreEventType::AddPoints));
}
}
}

fn despawn_projectiles(
mut commands: Commands,
projectile_query: Query<(Entity, &Transform, &Velocity), With<Projectile>>,
tile_query: TileQuery,
Expand Down
7 changes: 7 additions & 0 deletions src/game/constants.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
use bevy::prelude::Vec2;
use bevy_rapier2d::prelude::Group;

pub const TILE_SIZE: Vec2 = Vec2::new(32., 32.);
pub const GRID_SIZE: Vec2 = Vec2::new(100., 100.);
pub const HALF_TILE_SIZE: Vec2 = Vec2::new(TILE_SIZE.x * 0.5, TILE_SIZE.y * 0.5);
pub const HALF_GRID_SIZE: Vec2 = Vec2::new(GRID_SIZE.x * 0.5, GRID_SIZE.y * 0.5);

pub const PLAYER_GROUP: Group = Group::GROUP_1;
pub const ENEMY_GROUP: Group = Group::GROUP_2;
pub const PROJECTILE_GROUP: Group = Group::GROUP_3;
pub const BUILDING_GROUP: Group = Group::GROUP_4;
pub const FIRE_BREATH_GROUP: Group = Group::GROUP_5;
10 changes: 7 additions & 3 deletions src/game/enemy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use crate::{
use super::{
combat::{AttackDamage, AttackTimer, Range, SpawnProjectileEvent},
resource_pool::{Health, ResourcePool},
BorderTile, InGameEntity, Player, HALF_TILE_SIZE, TILE_SIZE,
BorderTile, InGameEntity, Player, BUILDING_GROUP, ENEMY_GROUP, FIRE_BREATH_GROUP,
HALF_TILE_SIZE, TILE_SIZE,
};

pub(super) struct EnemyPlugin;
Expand Down Expand Up @@ -72,7 +73,7 @@ fn spawn_enemies(
let translation = tile_transform.translation.truncate().extend(1.);
let mut enemy_entity_commands = commands.spawn(EnemyBundle {
attack_damage: AttackDamage(5),
attack_timer: AttackTimer(Timer::from_seconds(5., TimerMode::Repeating)),
attack_timer: AttackTimer::new(4.),
behavior: Behavior::FollowPlayer {
distance: TILE_SIZE.x * 6.,
},
Expand All @@ -92,7 +93,10 @@ fn spawn_enemies(
collider: Collider::cuboid(HALF_TILE_SIZE.x, HALF_TILE_SIZE.y),
render_layers: RenderLayers::layer(GROUND_LAYER),
rigid_body: RigidBody::Dynamic,
collision_groups: CollisionGroups::new(Group::GROUP_2, Group::GROUP_2),
collision_groups: CollisionGroups::new(
ENEMY_GROUP,
ENEMY_GROUP | BUILDING_GROUP | FIRE_BREATH_GROUP,
),
});

enemy_entity_commands.insert((InGameEntity, LockedAxes::ROTATION_LOCKED, YSorted));
Expand Down
11 changes: 8 additions & 3 deletions src/game/fire_breath.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use bevy::{prelude::*, render::view::RenderLayers};
use bevy_particle_systems::*;
use bevy_rapier2d::prelude::{Collider, Sensor};
use bevy_rapier2d::prelude::{Collider, CollisionGroups, Sensor};

use crate::{
camera::{YSorted, SKY_LAYER},
Expand All @@ -10,7 +10,7 @@ use crate::{
use super::{
combat::ImpactDamage,
resource_pool::{Fire, ResourcePool},
InGameEntity, Player,
InGameEntity, Player, BUILDING_GROUP, ENEMY_GROUP, FIRE_BREATH_GROUP,
};

pub(super) struct FireBreathPlugin;
Expand Down Expand Up @@ -91,7 +91,12 @@ fn spawn_fire_breath(
damage: ImpactDamage(damage),
});

fire_breath_entity_commands.insert((InGameEntity, Playing, YSorted));
fire_breath_entity_commands.insert((
CollisionGroups::new(FIRE_BREATH_GROUP, BUILDING_GROUP | ENEMY_GROUP),
InGameEntity,
Playing,
YSorted,
));
}
}

Expand Down
87 changes: 81 additions & 6 deletions src/game/level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,28 @@ use bevy::{
prelude::*,
render::view::RenderLayers,
};
use bevy_rapier2d::prelude::*;
use noise::{NoiseFn, Perlin};
use pathfinding::prelude::Matrix;
use rand::random;
use rand::{random, seq::SliceRandom};

use crate::{
audio::PlayMusicEvent,
camera::BACKGROUND_LAYER,
game::{InGameEntity, GRID_SIZE, HALF_GRID_SIZE, TILE_SIZE},
audio::{PlayMusicEvent, SoundEffect},
camera::{YSorted, BACKGROUND_LAYER, GROUND_LAYER},
entity_cleanup,
game::{
InGameEntity, BUILDING_GROUP, ENEMY_GROUP, FIRE_BREATH_GROUP, GRID_SIZE, HALF_GRID_SIZE,
HALF_TILE_SIZE, TILE_SIZE,
},
AppState,
};

use super::{
combat::{AttackDamage, AttackTimer, Range},
resource_pool::{Health, ResourcePool},
Enemy,
};

pub(super) struct LevelPlugin;

impl Plugin for LevelPlugin {
Expand All @@ -29,7 +40,12 @@ impl Plugin for LevelPlugin {

app.add_systems(
OnEnter(AppState::InGame),
(spawn_level_tiles, play_background_music).chain(),
(spawn_level_tiles, spawn_buildings, play_background_music).chain(),
);

app.add_systems(
OnExit(AppState::InGame),
entity_cleanup::<With<SoundEffect>>,
);
}
}
Expand Down Expand Up @@ -92,6 +108,50 @@ fn spawn_level_tiles(mut commands: Commands, level_matrix: Res<LevelMatrix>) {
}
}

fn spawn_buildings(mut commands: Commands, level_matrix: Res<LevelMatrix>) {
const BUILDING_SPAWN_CHANCE: f32 = 0.01;

let grass_tiles: Vec<Vec2> = level_matrix
.items()
.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 mut rng = rand::thread_rng();

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::<Health>::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),
..default()
},
});

building_entity_commands.insert((InGameEntity, YSorted));
}
}
}

fn play_background_music(mut play_music_event_writer: EventWriter<PlayMusicEvent>) {
play_music_event_writer.send(PlayMusicEvent::new(
"theme2.ogg",
Expand All @@ -103,9 +163,24 @@ fn play_background_music(mut play_music_event_writer: EventWriter<PlayMusicEvent
));
}

#[derive(Resource)]
#[derive(Resource, Deref)]
pub struct LevelMatrix(Matrix<Tile>);

#[derive(Bundle)]
pub struct BuildingBundle {
pub active_collision_types: ActiveCollisionTypes,
pub attack_damage: AttackDamage,
pub attack_timer: AttackTimer,
pub collider: Collider,
pub collision_groups: CollisionGroups,
pub hitpoints: ResourcePool<Health>,
pub marker: Enemy,
pub range: Range,
pub sprite: SpriteBundle,
pub render_layers: RenderLayers,
pub rigid_body: RigidBody,
}

#[derive(Component)]
pub struct BorderTile;

Expand Down
4 changes: 2 additions & 2 deletions src/game/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
use super::{
resource_pool::{Fire, Health, ResourcePool},
score_system::Score,
InGameEntity,
InGameEntity, PLAYER_GROUP, PROJECTILE_GROUP,
};

pub(super) struct PlayerPlugin;
Expand Down Expand Up @@ -49,7 +49,7 @@ fn spawn_player(mut commands: Commands, asset_server: Res<AssetServer>) {
animation_indices: AnimationIndices::new(0, 2),
animation_timer: AnimationTimer::from_seconds(0.2),
collider: Collider::ball(80.5),
collision_groups: CollisionGroups::new(Group::GROUP_1, Group::GROUP_1 | Group::GROUP_3),
collision_groups: CollisionGroups::new(PLAYER_GROUP, PROJECTILE_GROUP),
fire_breath_resource: ResourcePool::<Fire>::new(100),
hitpoints: ResourcePool::<Health>::new(100),
score: Score::new(0, 1),
Expand Down
2 changes: 1 addition & 1 deletion src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ fn mouse_input(
// TODO: replace constant with sprite dimensions
let fire_position = player_transform.translation.truncate() + player_direction * 90.;

spawn_fire_breath_event_writer.send(SpawnFireBreathEvent::new(1000, fire_position));
spawn_fire_breath_event_writer.send(SpawnFireBreathEvent::new(1, fire_position));
}
}

Expand Down

0 comments on commit c590f99

Please sign in to comment.