diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 420e84a..6c099a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + cancel-in-progress: ${{ github.event_name != 'pull_request' }} jobs: conformance: @@ -46,7 +46,7 @@ jobs: - name: Commit Lint uses: gsactions/commit-message-checker@v1 with: - pattern: "^(feat|fix|refactor|style|test|docs|ci|chore): " + pattern: "^(feat|fix|refactor|tweak|style|test|docs|ci|chore): " error: "One of the commit messages doesn't conform to the commit style of the project." excludeTitle: "true" excludeDescription: "true" diff --git a/Cargo.toml b/Cargo.toml index dbeeb27..e1aa675 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,10 @@ members = [ "players/greek", ] +[workspace.lints.clippy] +derivable_impls = "allow" +type_complexity = "allow" + [profile.dev] opt-level = 1 diff --git a/enemies/sweet/Cargo.toml b/enemies/sweet/Cargo.toml index 66cede6..fe67caf 100644 --- a/enemies/sweet/Cargo.toml +++ b/enemies/sweet/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" [dependencies] mythmallow = { path = "../../game", package = "mythmallow-game" } survival-mode = { path = "../../modes/survival", package = "mythmallow-mode-survival" } + +[lints] +workspace = true diff --git a/enemies/sweet/src/gummy_bear.rs b/enemies/sweet/src/gummy_bear.rs index 110317c..f9ddbfe 100644 --- a/enemies/sweet/src/gummy_bear.rs +++ b/enemies/sweet/src/gummy_bear.rs @@ -6,11 +6,27 @@ use { }, }; -/// Tag component for the enemy "Gummy Bear". -#[derive(Component, Debug, Reflect)] +/// Size of the enemy. +pub const SIZE: f32 = 15.00; + +/// Health of the enemy. +pub const HEALTH: f32 = 5.00; + +/// Speed of the enemy. +pub const SPEED: f32 = 100.00; + +/// Contact damage of the enemy. +pub const CONTACT_DAMAGE: f32 = 3.00; + +/// Cooldown of contact damage of the enemy. +pub const CONTACT_DAMAGE_COOLDOWN: Duration = Duration::from_millis(1000); + +/// Component for the enemy "Gummy Bear". +#[derive(Clone, Component, Debug, Default, Reflect)] +#[reflect(Component)] pub struct GummyBear; -impl Munchie for GummyBear { +impl IEnemy for GummyBear { fn id(&self) -> SmolStr { "gummy-bear".into() } @@ -19,12 +35,24 @@ impl Munchie for GummyBear { "Gummy Bear".into() } + fn contact_damage(&self) -> Option<(Damage, DamageCooldown)> { + Some((Damage(CONTACT_DAMAGE), DamageCooldown::new(CONTACT_DAMAGE_COOLDOWN))) + } + + fn health(&self) -> Health { + Health(HEALTH) + } + + fn speed(&self) -> Speed { + Speed(SPEED) + } + fn collider(&self) -> Collider { Collider::ball(SIZE) } fn spawn(&self, world: &mut World, position: Position) { - world.run_system_once_with(position, spawn); + world.run_system_once_with((self.clone(), position), spawn); } } @@ -34,9 +62,8 @@ pub struct GummyBearPlugin; impl Plugin for GummyBearPlugin { fn build(&self, app: &mut App) { // Register the enemy. - let mut enemy_registry = ENEMY_REGISTRY.lock().unwrap(); - enemy_registry.register(SweetMunchiesPack, GummyBear).add_tag(MELEE_ENEMY_TAG); - drop(enemy_registry); + let mut enemy_registry = app.world.resource_mut::(); + enemy_registry.register(SweetEnemyPack, GummyBear).add_tag(MELEE_ENEMY_TAG); // Register components. app.register_type::(); @@ -46,55 +73,25 @@ impl Plugin for GummyBearPlugin { } } -/// Size of the enemy. -pub const SIZE: f32 = 15.00; - -/// Initial damage of the enemy. -pub const INITIAL_DAMAGE: f32 = 3.00; - -/// Initial health of the enemy. -pub const INITIAL_HEALTH: f32 = 5.00; - -/// Initial speed of the enemy. -pub const INITIAL_SPEED: f32 = 100.00; - /// Spawns the enemy. pub fn spawn( - In(position): In, + In((enemy, position)): In<(GummyBear, Position)>, mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, mut counter: ResMut, ) { - counter.increment(); - commands.spawn(( - GummyBear, - EnemyBundle { - // Tags - name: Name::new(format!("Enemy {} [Gummy Bear]", counter.get())), - tag: Enemy, - // Properties - damage: Damage(INITIAL_DAMAGE), - health: Health(INITIAL_HEALTH), - speed: Speed(INITIAL_SPEED), - // Combat - remaining_health: RemainingHealth(INITIAL_HEALTH), - // Physics - body: RigidBody::Dynamic, - restitution: Restitution::PERFECTLY_INELASTIC, - position, - collider: GummyBear.collider(), - layers: CollisionLayers::new( - [Layer::Enemy], - [Layer::MapBound, Layer::Enemy, Layer::PlayerHitBox], - ), - // Texture - mesh: MaterialMesh2dBundle { - mesh: meshes.add(shape::Circle::new(SIZE).into()).into(), - material: materials.add(ColorMaterial::from(Color::RED)), - transform: Transform::from_translation(Vec3::new(position.x, position.y, 1.00)), - ..default() - }, - }, - )); + let mesh = MaterialMesh2dBundle { + mesh: meshes.add(shape::Circle::new(SIZE).into()).into(), + material: materials.add(ColorMaterial::from(Color::RED)), + transform: Transform::from_translation(position.extend(Depth::Enemy.z())), + ..default() + }; + + EnemyBundle::builder() + .enemy(enemy) + .position(position) + .mesh(mesh) + .build() + .spawn(&mut commands, &mut counter); } diff --git a/enemies/sweet/src/pack.rs b/enemies/sweet/src/pack.rs index e847a68..d828ca3 100644 --- a/enemies/sweet/src/pack.rs +++ b/enemies/sweet/src/pack.rs @@ -1,10 +1,11 @@ use mythmallow::prelude::*; -/// Sweet enemies. -#[derive(Debug)] -pub struct SweetMunchiesPack; +/// Resource for "Sweet" enemy pack. +#[derive(Debug, Default, Reflect, Resource)] +#[reflect(Resource)] +pub struct SweetEnemyPack; -impl MunchiePack for SweetMunchiesPack { +impl IEnemyPack for SweetEnemyPack { fn id(&self) -> SmolStr { "sweet".into() } diff --git a/enemies/sweet/src/plugin.rs b/enemies/sweet/src/plugin.rs index 9c5df7a..82667ff 100644 --- a/enemies/sweet/src/plugin.rs +++ b/enemies/sweet/src/plugin.rs @@ -3,7 +3,7 @@ use { mythmallow::prelude::*, }; -/// Plugin for managing the enemies from Sweet enemy pack. +/// Plugin for managing the enemies from "Sweet" enemy pack. pub struct SweetEnemiesPlugin; impl Plugin for SweetEnemiesPlugin { diff --git a/enemies/sweet/src/prelude.rs b/enemies/sweet/src/prelude.rs index 30e7a73..f1ffdd9 100644 --- a/enemies/sweet/src/prelude.rs +++ b/enemies/sweet/src/prelude.rs @@ -1,5 +1,5 @@ pub use crate::{ gummy_bear::GummyBear, - pack::SweetMunchiesPack, + pack::SweetEnemyPack, plugin::SweetEnemiesPlugin, }; diff --git a/game/Cargo.toml b/game/Cargo.toml index f90e68d..a433fc8 100644 --- a/game/Cargo.toml +++ b/game/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] bevy = { version = "0.12", features = ["serialize"] } bevy-persistent = { version = "0.4" } -bevy_editor_pls = { version = "0.6", optional = true } +bevy_editor_pls = { version = "0.7", optional = true } bevy_prng = { version = "0.2", features = ["rand_chacha"] } bevy_rand = { version = "0.4" } bevy_xpbd_2d = { version = "0.3" } @@ -19,6 +19,7 @@ smallvec = { version = "1.11", features = ["serde"] } smol_str = { version = "0.2" } strum = { version = "0.25" } strum_macros = { version = "0.25" } +typed-builder = { version = "0.18" } [target.'cfg(not(target_family = "wasm"))'.dependencies] bevy-persistent-windows = { version = "0.4" } @@ -39,3 +40,6 @@ native-release = ["native"] wasm = ["bevy-persistent/json"] wasm-development = ["wasm", "development"] wasm-release = ["wasm"] + +[lints] +workspace = true diff --git a/game/src/combat/components.rs b/game/src/combat/components.rs index 1b48f2b..e94ae6f 100644 --- a/game/src/combat/components.rs +++ b/game/src/combat/components.rs @@ -1,11 +1,87 @@ use crate::prelude::*; -/// Tag component for basic attacks. +/// Tag component for attacks. #[derive(Component, Debug, Reflect)] pub struct Attack; +/// Component for cooldown of applying damage. +#[derive(Component, Debug, Deref, DerefMut, Reflect)] +pub struct DamageCooldown { + pub duration: Duration, +} + +impl DamageCooldown { + /// Creates a new damage cooldown. + pub fn new(duration: Duration) -> DamageCooldown { + DamageCooldown { duration } + } +} + + +/// Tag component for projectiles. +#[derive(Component, Debug, Reflect)] +pub struct Projectile; + + +/// Bundle for projectiles. +#[derive(Bundle, TypedBuilder)] +pub struct ProjectileBundle { + pub mesh: MaterialMesh2dBundle, + pub collider: Collider, + pub position: Position, + pub velocity: LinearVelocity, + pub damage: Damage, +} + +impl ProjectileBundle { + /// Spawns the projectile. + fn spawn<'w, 's, 'a>( + self, + commands: &'a mut Commands<'w, 's>, + layers: CollisionLayers, + additional_components: impl Bundle, + ) -> EntityCommands<'w, 's, 'a> { + commands.spawn(( + // Tags + Name::new("Projectile"), + Projectile, + // Projectile + self, + additional_components, + // Physics + RigidBody::Dynamic, + layers, + )) + } + + /// Spawns the projectile toward the player. + pub fn spawn_toward_player<'w, 's, 'a>( + self, + commands: &'a mut Commands<'w, 's>, + ) -> EntityCommands<'w, 's, 'a> { + let layers = CollisionLayers::new( + [Layer::Projectile, Layer::DamagePlayer], + [Layer::MapBound, Layer::MapObstacle, Layer::PlayerHitBox], + ); + self.spawn(commands, layers, DamagePlayerOnContact) + } + + /// Spawns the projectile toward enemies. + pub fn spawn_toward_enemies<'w, 's, 'a>( + self, + commands: &'a mut Commands<'w, 's>, + ) -> EntityCommands<'w, 's, 'a> { + let layers = CollisionLayers::new( + [Layer::Projectile, Layer::DamageEnemies], + [Layer::MapBound, Layer::MapObstacle, Layer::EnemyHitBox], + ); + self.spawn(commands, layers, DamageEnemiesOnContact) + } +} + + /// Component for remaining health. #[derive(Clone, Copy, Component, Debug, Deref, DerefMut, Reflect)] pub struct RemainingHealth(pub f32); diff --git a/game/src/combat/mod.rs b/game/src/combat/mod.rs index fc06ca5..815062f 100644 --- a/game/src/combat/mod.rs +++ b/game/src/combat/mod.rs @@ -1,3 +1,4 @@ pub mod components; pub mod plugin; pub mod systems; +pub mod utils; diff --git a/game/src/combat/plugin.rs b/game/src/combat/plugin.rs index 0ccb983..46270ec 100644 --- a/game/src/combat/plugin.rs +++ b/game/src/combat/plugin.rs @@ -9,16 +9,17 @@ pub struct CombatPlugin; impl Plugin for CombatPlugin { fn build(&self, app: &mut App) { // Register components. - app.register_type::(); + app.register_type::(); app.register_type::>(); + app.register_type::(); + app.register_type::(); // Add systems. app.add_systems(PreUpdate, cooldown::.in_set(GameplaySystems::Combat)); + app.add_systems(Update, (damage_player, damage_enemies).in_set(GameplaySystems::Combat)); app.add_systems( - Update, - (damage_player_on_contact_with_enemies, player_death) - .chain() - .in_set(GameplaySystems::Combat), + PostUpdate, + (player_death, enemy_death, despawn_projectiles).in_set(GameplaySystems::Combat), ); } } diff --git a/game/src/combat/systems.rs b/game/src/combat/systems.rs index cec7d1f..f6cd629 100644 --- a/game/src/combat/systems.rs +++ b/game/src/combat/systems.rs @@ -1,45 +1,77 @@ use crate::prelude::*; -/// Damages the player for every enemy it's touching. -pub fn damage_player_on_contact_with_enemies( +/// Damages the player. +pub fn damage_player( mut commands: Commands, mut player_query: Query<&mut RemainingHealth, With>, - mut player_hit_box_query: Query>, - enemy_query: Query<&Damage, (Without, With, Without>)>, + player_hit_box_query: Query<&Parent, With>, + player_damage_query: Query< + (Entity, &Damage, Option<&DamageCooldown>), + (With, Without>), + >, mut collision_event_reader: EventReader, ) { - let mut player_remaining_health = match player_query.get_single_mut() { - Ok(query_result) => query_result, - Err(_) => return, - }; - - let player_sensor_entity = match player_hit_box_query.get_single_mut() { - Ok(query_result) => query_result, - Err(_) => return, + let mut apply_damage_if_applicable = |player_hit_box_entity, player_damage_entity| { + let (damaging_entity, damage, damage_cooldown) = + match player_damage_query.get(player_damage_entity) { + Ok(query_result) => query_result, + Err(_) => return, + }; + let remaining_health = match player_hit_box_query.get(player_hit_box_entity) { + Ok(parent) => player_query.get_mut(parent.get()), + Err(_) => return, + }; + if let Ok(mut remaining_health) = remaining_health { + remaining_health.0 -= damage.0; + if let Some(damage_cooldown) = damage_cooldown { + commands + .entity(damaging_entity) + .insert(Cooldown::::new(damage_cooldown.duration)); + } + } }; - for Collision(contact) in collision_event_reader.read() { - if contact.entity1 == player_sensor_entity || contact.entity2 == player_sensor_entity { - let enemy_entity = if contact.entity1 == player_sensor_entity { - contact.entity2 - } else { - contact.entity1 - }; + for Collision(contacts) in collision_event_reader.read().cloned() { + apply_damage_if_applicable(contacts.entity1, contacts.entity2); + apply_damage_if_applicable(contacts.entity2, contacts.entity1); + } +} - let enemy_damage = match enemy_query.get(enemy_entity) { +/// Damages the enemies. +pub fn damage_enemies( + mut commands: Commands, + mut enemy_query: Query<&mut RemainingHealth, With>, + enemy_hit_box_query: Query<&Parent, With>, + enemy_damage_query: Query< + (Entity, &Damage, Option<&DamageCooldown>), + (With, Without>), + >, + mut collision_event_reader: EventReader, +) { + let mut apply_damage_if_applicable = |enemy_hit_box_entity, enemy_damage_entity| { + let (damaging_entity, damage, damage_cooldown) = + match enemy_damage_query.get(enemy_damage_entity) { Ok(query_result) => query_result, - Err(_) => continue, + Err(_) => return, }; - - player_remaining_health.0 -= enemy_damage.0; - - commands - .entity(enemy_entity) - .insert(Cooldown::::new(Timer::from_seconds(1.00, TimerMode::Once))); - - break; + let remaining_health = match enemy_hit_box_query.get(enemy_hit_box_entity) { + Ok(parent) => enemy_query.get_mut(parent.get()), + Err(_) => return, + }; + if let Ok(mut remaining_health) = remaining_health { + remaining_health.0 -= damage.0; + if let Some(damage_cooldown) = damage_cooldown { + commands + .entity(damaging_entity) + .insert(Cooldown::::new(damage_cooldown.duration)); + } } + }; + + for Collision(contacts) in collision_event_reader.read().cloned() { + apply_damage_if_applicable(contacts.entity1, contacts.entity2); + apply_damage_if_applicable(contacts.entity2, contacts.entity1); } } @@ -59,3 +91,31 @@ pub fn player_death( next_game_state.set(GameState::Over); } } + +/// Handles enemy death. +pub fn enemy_death( + mut commands: Commands, + enemy_query: Query<(Entity, &RemainingHealth), With>, +) { + for (enemy_entity, enemy_remaining_health) in enemy_query.iter() { + if enemy_remaining_health.0 <= 0.00 { + commands.entity(enemy_entity).despawn_recursive(); + } + } +} + + +/// Despawns the projectiles on contact. +pub fn despawn_projectiles( + mut commands: Commands, + projectile_query: Query>, + mut collision_started_event_reader: EventReader, +) { + for CollisionStarted(entity1, entity2) in collision_started_event_reader.read().cloned() { + if projectile_query.get(entity1).is_ok() { + commands.entity(entity1).despawn_recursive(); + } else if projectile_query.get(entity2).is_ok() { + commands.entity(entity2).despawn_recursive(); + } + } +} diff --git a/game/src/combat/utils.rs b/game/src/combat/utils.rs new file mode 100644 index 0000000..22b4f38 --- /dev/null +++ b/game/src/combat/utils.rs @@ -0,0 +1,39 @@ +use crate::prelude::*; + + +/// Finds the enemies in range from a position ordered by their distance. +pub fn find_enemies_in_range_sorted_by_distance( + spatial_query: &SpatialQuery, + position: &Position, + area: &Collider, + enemy_hit_box_query: &Query<&Position, With>, +) -> Vec<(Entity, Position, f32)> { + let intersections = spatial_query.shape_intersections( + area, + position.xy(), + 0.00, + SpatialQueryFilter::new().with_masks([Layer::EnemyHitBox]), + ); + + let mut enemies_in_range = intersections + .iter() + .filter_map(|&enemy_hit_box_entity| { + enemy_hit_box_query + .get(enemy_hit_box_entity) + .map(|&enemy_hit_box_position| { + ( + enemy_hit_box_entity, + enemy_hit_box_position, + enemy_hit_box_position.distance(position.xy()), + ) + }) + .ok() + }) + .collect::>(); + + enemies_in_range.sort_by(|(_, _, distance1), (_, _, distance2)| { + distance1.partial_cmp(distance2).unwrap_or(Ordering::Equal) + }); + + enemies_in_range +} diff --git a/game/src/configuration/resources.rs b/game/src/configuration/resources.rs index 65b9d37..c492980 100644 --- a/game/src/configuration/resources.rs +++ b/game/src/configuration/resources.rs @@ -220,6 +220,9 @@ impl Args { pub struct GeneralSettings { pub pause_on_losing_focus: bool, pub show_diagnostics_overlay: bool, + + #[cfg(feature = "development")] + pub debug_physics: bool, } impl GeneralSettings { @@ -250,7 +253,13 @@ impl GeneralSettings { impl Default for GeneralSettings { fn default() -> GeneralSettings { - GeneralSettings { pause_on_losing_focus: true, show_diagnostics_overlay: false } + GeneralSettings { + pause_on_losing_focus: true, + show_diagnostics_overlay: false, + + #[cfg(feature = "development")] + debug_physics: false, + } } } diff --git a/game/src/core/depths.rs b/game/src/core/depths.rs new file mode 100644 index 0000000..cba0eca --- /dev/null +++ b/game/src/core/depths.rs @@ -0,0 +1,19 @@ +#[derive(Clone, Copy, Debug)] +#[repr(u8)] +pub enum Depth { + Bottom = 0, + + Map, + Enemy, + Player, + Item, + Projectile, + + Top, +} + +impl Depth { + pub fn z(self) -> f32 { + (self as u8) as f32 + } +} diff --git a/game/src/core/mod.rs b/game/src/core/mod.rs index 2b11f9c..d134469 100644 --- a/game/src/core/mod.rs +++ b/game/src/core/mod.rs @@ -1,3 +1,4 @@ +pub mod depths; pub mod plugin; pub mod resources; pub mod sets; diff --git a/game/src/core/sets/gameplay.rs b/game/src/core/sets/gameplay.rs index 777ff61..fa8b05b 100644 --- a/game/src/core/sets/gameplay.rs +++ b/game/src/core/sets/gameplay.rs @@ -8,6 +8,7 @@ pub enum GameplaySystems { Enemy, GameMode, Input, + Item, Movement, Physics, Player, diff --git a/game/src/core/sets/initialization.rs b/game/src/core/sets/initialization.rs index 63f3b6b..f541246 100644 --- a/game/src/core/sets/initialization.rs +++ b/game/src/core/sets/initialization.rs @@ -3,8 +3,10 @@ use crate::prelude::*; /// Systems to run when initializing the game. #[derive(Clone, Copy, Debug, EnumIter, Eq, Hash, PartialEq, SystemSet)] pub enum InitializationSystems { + First, GameMode, Player, + Last, Done, } diff --git a/game/src/core/sets/restart.rs b/game/src/core/sets/restart.rs index 5493fb3..466f6d7 100644 --- a/game/src/core/sets/restart.rs +++ b/game/src/core/sets/restart.rs @@ -3,12 +3,14 @@ use crate::prelude::*; /// Systems to run when restarting the game. #[derive(Clone, Copy, Debug, EnumIter, Eq, Hash, PartialEq, SystemSet)] pub enum RestartSystems { + First, Enemy, Camera, GameMode, Inventory, Map, Player, + Last, Done, } diff --git a/game/src/enemy/components.rs b/game/src/enemy/components.rs index 8f1bdaf..fdbee52 100644 --- a/game/src/enemy/components.rs +++ b/game/src/enemy/components.rs @@ -2,28 +2,92 @@ use crate::prelude::*; /// Tag component for enemies. -#[derive(Component, Debug, Reflect)] +#[derive(Component, Debug, Default, Reflect)] pub struct Enemy; +/// Tag component for hit boxes of enemies. +#[derive(Component, Debug, Default, Reflect)] +pub struct EnemyHitBox; + +impl EnemyHitBox { + pub fn bundle(collider: Collider) -> impl Bundle { + ( + // Tags + Name::new("Hit Box"), + EnemyHitBox, + // Physics + collider, + CollisionLayers::new([Layer::EnemyHitBox], [Layer::DamageEnemies]), + Sensor, + ) + } +} + + +/// Tag component for entities that apply damage to enemies on contact. +#[derive(Component, Debug, Default, Reflect)] +pub struct DamageEnemiesOnContact; + + /// Bundle for enemies. -#[derive(Bundle)] -pub struct EnemyBundle { - // Tags - pub name: Name, - pub tag: Enemy, - // Properties - pub damage: Damage, - pub health: Health, - pub speed: Speed, - // Combat - pub remaining_health: RemainingHealth, - // Physics - pub body: RigidBody, - pub restitution: Restitution, +#[derive(Bundle, TypedBuilder)] +pub struct EnemyBundle { + pub enemy: E, pub position: Position, - pub collider: Collider, - pub layers: CollisionLayers, - // Texture pub mesh: MaterialMesh2dBundle, } + +impl EnemyBundle { + /// Spawns the enemy. + pub fn spawn<'w, 's, 'a>( + self, + commands: &'a mut Commands<'w, 's>, + counter: &mut EnemyCounter, + ) -> EntityCommands<'w, 's, 'a> { + counter.increment(); + + let name = self.enemy.name(); + let contact_damage = self.enemy.contact_damage(); + let health = self.enemy.health(); + let speed = self.enemy.speed(); + let collider = self.enemy.collider(); + + let mut collision_layers = + CollisionLayers::new([Layer::Enemy], [Layer::MapBound, Layer::Enemy]); + + if contact_damage.is_some() { + collision_layers = collision_layers.add_group(Layer::DamagePlayer); + collision_layers = collision_layers.add_mask(Layer::PlayerHitBox); + } + + let mut enemy = commands.spawn(( + // Tags + Name::new(format!("Enemy {} [{}]", counter.get(), name)), + Enemy, + // Properties + self, + health, + speed, + // Combat + RemainingHealth(*health), + // Physics + RigidBody::Dynamic, + LinearVelocity::ZERO, + Restitution::PERFECTLY_INELASTIC, + LockedAxes::ROTATION_LOCKED, + collider.clone(), + collision_layers, + )); + + enemy.with_children(|parent| { + parent.spawn(EnemyHitBox::bundle(collider)); + }); + + if let Some((damage, cooldown)) = contact_damage { + enemy.insert((DamagePlayerOnContact, damage, cooldown)); + } + + enemy + } +} diff --git a/game/src/enemy/interfaces.rs b/game/src/enemy/interfaces.rs index b8af509..f5d900f 100644 --- a/game/src/enemy/interfaces.rs +++ b/game/src/enemy/interfaces.rs @@ -1,7 +1,7 @@ use crate::prelude::*; /// Interface for enemy packs. -pub trait MunchiePack: Any + Debug + Send + Sync + 'static { +pub trait IEnemyPack: Any + Debug + Send + Sync + 'static { /// Gets the unique identifier of the enemy pack. fn id(&self) -> SmolStr; /// Gets the name of the enemy pack. @@ -15,15 +15,23 @@ pub trait MunchiePack: Any + Debug + Send + Sync + 'static { } /// Interface for enemies. -pub trait Munchie: Debug + Send + Sync + 'static { +pub trait IEnemy: Debug + Send + Sync + 'static { /// Gets the unique identifier of the enemy. fn id(&self) -> SmolStr; /// Gets the name of the enemy. fn name(&self) -> SmolStr; + /// Gets the contact damage of the enemy. + fn contact_damage(&self) -> Option<(Damage, DamageCooldown)> { + None + } + /// Gets the health of the enemy. + fn health(&self) -> Health; + /// Gets the speed of the enemy. + fn speed(&self) -> Speed; + /// Gets the collider of the enemy. fn collider(&self) -> Collider; - /// Spawns the enemy. fn spawn(&self, world: &mut World, position: Position); } diff --git a/game/src/enemy/plugin.rs b/game/src/enemy/plugin.rs index f8c730f..c518bd9 100644 --- a/game/src/enemy/plugin.rs +++ b/game/src/enemy/plugin.rs @@ -10,6 +10,15 @@ impl Plugin for EnemyPlugin { fn build(&self, app: &mut App) { // Register components. app.register_type::(); + app.register_type::(); + app.register_type::(); + + // Register resources. + app.register_type::(); + app.register_type::(); + + // Initialize registry. + app.init_resource::(); // Add systems. app.add_systems( diff --git a/game/src/enemy/registry.rs b/game/src/enemy/registry.rs index d0829fc..60b9db2 100644 --- a/game/src/enemy/registry.rs +++ b/game/src/enemy/registry.rs @@ -1,115 +1,148 @@ use crate::prelude::*; -/// Registry for enemies. -pub static ENEMY_REGISTRY: Mutex = Mutex::new(EnemyRegistry::new()); -/// Container for enemy registry. -#[derive(Default, Deref, DerefMut, Resource)] -pub struct EnemyRegistry(pub Vec<(Arc, Vec)>); +/// Container for the enemy registry. +#[derive(Debug, Default, Deref, Resource)] +pub struct EnemyRegistry(Vec); impl EnemyRegistry { - /// Creates a new enemy registry. - pub const fn new() -> EnemyRegistry { - EnemyRegistry(Vec::new()) - } -} - -impl EnemyRegistry { - /// Registers an enemy to enemy registry. - pub fn register( - &mut self, - pack: impl MunchiePack, - enemy: impl Munchie, - ) -> &mut EnemyRegistryEntry { + /// Registers an enemy to the enemy registry. + pub fn register(&mut self, pack: impl IEnemyPack, enemy: impl IEnemy) -> &mut RegisteredEnemy { let pack_id = pack.id(); - let pack_name = pack.name(); + let pack_index = + self.iter().position(|entry| entry.pack.id() == pack_id).unwrap_or_else(|| { + let index = self.len(); + self.0.push(EnemyRegistryEntry::new(pack)); + index + }); + + let enemies = &mut self.0[pack_index].enemies; - let pack_index = match self.iter_mut().position(|(candidate, _)| candidate.id() == pack_id) - { - Some(pack_index) => pack_index, + let enemy_id = enemy.id(); + let enemy_index = match enemies.iter_mut().position(|enemy| enemy.id() == enemy_id) { + Some(index) => { + log::warn!( + "tried to register {:?} from {:?} enemy pack to the enemy registry again", + enemy_id, + pack_id, + ); + index + }, None => { - let pack_index = self.len(); - self.push((Arc::new(pack), Vec::new())); - pack_index + log::info!( + "registered {:?} from {:?} enemy pack to the enemy registry", + enemy_id, + pack_id, + ); + let index = enemies.len(); + enemies.push(RegisteredEnemy::new(enemy)); + index }, }; - let pack_entries = &mut self.0[pack_index].1; - let enemy_id = enemy.id(); - let enemy_name = enemy.name(); - - let enemy_index = - match pack_entries.iter_mut().position(|candidate| candidate.id() == enemy_id) { - Some(enemy_index) => { - log::warn!( - "tried to register {:?} from {:?} enemy pack to enemy registry again", - enemy_name, - pack_name, - ); - - enemy_index - }, - None => { - log::info!( - "registered {:?} from {:?} enemy pack to enemy registry", - enemy_name, - pack_name, - ); - - let enemy_index = pack_entries.len(); - pack_entries.push(EnemyRegistryEntry::new(enemy)); - enemy_index - }, - }; - - &mut pack_entries[enemy_index] + &mut enemies[enemy_index] + } +} + +impl EnemyRegistry { + /// Gets the number of enemy packs in the enemy registry. + pub fn number_of_packs(&self) -> usize { + self.0.len() + } + + /// Gets the number of enemies in the enemy registry. + pub fn number_of_enemies(&self) -> usize { + self.0.iter().map(|entry| entry.enemies.len()).sum() } } impl Index for EnemyRegistry { - type Output = (Arc, Vec); + type Output = EnemyRegistryEntry; - fn index( - &self, - index: SelectedEnemyPackIndex, - ) -> &(Arc, Vec) { - &self.deref()[index.0] + fn index(&self, enemy_pack_index: SelectedEnemyPackIndex) -> &EnemyRegistryEntry { + &self.0[*enemy_pack_index] } } -/// Container for enemy registry entries. -#[derive(Clone, Debug)] + +/// Container for the entries of the enemy registry. +#[derive(Debug)] pub struct EnemyRegistryEntry { - pub enemy: Arc, - pub tags: SmallVec<[SmolStr; 3]>, + pub pack: RegisteredEnemyPack, + pub enemies: Vec, } impl EnemyRegistryEntry { - /// Create a new entry for an enemy. - pub fn new(enemy: E) -> EnemyRegistryEntry { - EnemyRegistryEntry { enemy: Arc::new(enemy), tags: SmallVec::new() } + /// Creates a new enemy registry entry. + pub fn new(pack: impl IEnemyPack) -> EnemyRegistryEntry { + EnemyRegistryEntry { pack: RegisteredEnemyPack::new(pack), enemies: Vec::new() } } } -impl EnemyRegistryEntry { - /// Add a tag to the item. - pub fn add_tag(&mut self, tag: impl ToString) -> &mut EnemyRegistryEntry { +impl Deref for EnemyRegistryEntry { + type Target = RegisteredEnemyPack; + + fn deref(&self) -> &RegisteredEnemyPack { + &self.pack + } +} + + +/// Container for registered enemy packs. +#[derive(Debug)] +pub struct RegisteredEnemyPack { + pub pack: Arc, +} + +impl RegisteredEnemyPack { + /// Creates a new registered enemy pack. + pub fn new(pack: impl IEnemyPack) -> RegisteredEnemyPack { + RegisteredEnemyPack { pack: Arc::new(pack) } + } +} + +impl Deref for RegisteredEnemyPack { + type Target = Arc; + + fn deref(&self) -> &Arc { + &self.pack + } +} + + +/// Container for registered enemies. +#[derive(Debug)] +pub struct RegisteredEnemy { + pub enemy: Arc, + pub tags: SmallVec<[SmolStr; 3]>, +} + +impl RegisteredEnemy { + /// Creates a new registered enemy. + pub fn new(enemy: impl IEnemy) -> RegisteredEnemy { + RegisteredEnemy { enemy: Arc::new(enemy), tags: SmallVec::new() } + } +} + +impl RegisteredEnemy { + /// Adds a tag to the enemy. + pub fn add_tag(&mut self, tag: impl ToString) -> &mut RegisteredEnemy { self.tags.push(tag.to_string().into()); self } } -impl EnemyRegistryEntry { - /// Gets if the enemy has the tag. +impl RegisteredEnemy { + /// Gets if the enemy has a tag. pub fn has_tag(&self, tag: &str) -> bool { self.tags.iter().any(|candidate| candidate == tag) } } -impl Deref for EnemyRegistryEntry { - type Target = Arc; +impl Deref for RegisteredEnemy { + type Target = Arc; - fn deref(&self) -> &Arc { + fn deref(&self) -> &Arc { &self.enemy } } diff --git a/game/src/enemy/resources.rs b/game/src/enemy/resources.rs index 0f3c3bf..639bd4e 100644 --- a/game/src/enemy/resources.rs +++ b/game/src/enemy/resources.rs @@ -6,11 +6,6 @@ use crate::prelude::*; pub struct SelectedEnemyPackIndex(pub usize); -/// Resource for the selected enemy pack. -#[derive(Clone, Debug, Deref, Resource)] -pub struct SelectedEnemyPack(pub (Arc, Vec)); - - /// Resource for counting spawned enemies. #[derive(Debug, Default, Reflect, Resource)] pub struct EnemyCounter(usize); @@ -58,7 +53,7 @@ pub struct EnemySpawn { /// Delay for the first spawn. pub delay: Timer, /// Enemy to spawn. - pub enemy: Arc, + pub enemy: Arc, /// Group size. pub count: u32, /// Optional spawn interval within the group. @@ -90,7 +85,7 @@ pub struct EnemySpawn { impl EnemySpawn { /// Creates a new enemy spawn. - pub fn new(delay: Duration, enemy: &Arc) -> EnemySpawn { + pub fn new(delay: Duration, enemy: &Arc) -> EnemySpawn { EnemySpawn { delay: Timer::new(delay, TimerMode::Once), enemy: Arc::clone(enemy), diff --git a/game/src/enemy/systems.rs b/game/src/enemy/systems.rs index 683c5c6..2ac2f25 100644 --- a/game/src/enemy/systems.rs +++ b/game/src/enemy/systems.rs @@ -11,13 +11,14 @@ pub fn initialize_enemy_counter(mut commands: Commands) { /// Initializes the enemy spawn pattern. pub fn initialize_enemy_spawn_pattern(world: &mut World) { - let enemy_registry = ENEMY_REGISTRY.lock().unwrap(); - let selection = world.resource::(); - let spawn_pattern = enemy_registry[*selection].0.spawn_pattern(world).unwrap_or_else(|| { - let game_mode_registry = GAME_MODE_REGISTRY.lock().unwrap(); - let selected_game_mode_index = world.resource::(); - game_mode_registry[*selected_game_mode_index].default_enemy_spawn_pattern(world) - }); + let enemy_registry = world.resource::(); + let selected_enemy_pack_index = world.resource::(); + let spawn_pattern = + enemy_registry[*selected_enemy_pack_index].pack.spawn_pattern(world).unwrap_or_else(|| { + let game_mode_registry = world.resource::(); + let selected_game_mode_index = world.resource::(); + game_mode_registry[*selected_game_mode_index].default_enemy_spawn_pattern(world) + }); log::info!("enemy spawn pattern for the level:\n{:#?}", spawn_pattern); world.insert_resource(spawn_pattern); } @@ -190,7 +191,7 @@ pub fn spawn_enemy(world: &mut World, map_bounds: &MapBounds, spawn: &mut EnemyS let enemy = &spawn.enemy; let desired_enemy_transform = - Transform::from_translation(Vec3::new(enemy_position.x, enemy_position.y, 1.00)); + Transform::from_translation(enemy_position.extend(Depth::Enemy.z())); let found_enemy_transform = world .run_system_once_with((desired_enemy_transform, enemy.collider(), 0.25), find_free_space); @@ -229,7 +230,9 @@ pub fn find_free_space( ); if intersections.is_empty() { - return Some(target_transform.with_translation(target_position.extend(1.0))); + return Some( + target_transform.with_translation(target_position.extend(Depth::Enemy.z())), + ); } else { for entity in intersections { let Ok((hit_collider, hit_transform)) = query.get(entity) else { @@ -273,7 +276,6 @@ pub fn clear_enemy_counter(mut commands: Commands) { /// Clears the enemy pack selection. pub fn clear_enemy_pack_selection(mut commands: Commands) { commands.remove_resource::(); - commands.remove_resource::(); } diff --git a/game/src/input/actions/global.rs b/game/src/input/actions/global.rs index 56e9c5c..e89b380 100644 --- a/game/src/input/actions/global.rs +++ b/game/src/input/actions/global.rs @@ -5,6 +5,9 @@ use crate::prelude::*; pub enum GlobalAction { ToggleFullscreen, ToggleDiagnosticsOverlay, + + #[cfg(feature = "development")] + TogglePhysicsDebug, } impl GlobalAction { @@ -14,10 +17,16 @@ impl GlobalAction { app.add_plugins(InputManagerPlugin::::default()); // Create the input map. - let input_map = InputMap::new([ - (KeyCode::F11, GlobalAction::ToggleFullscreen), - (KeyCode::F10, GlobalAction::ToggleDiagnosticsOverlay), - ]); + let mut input_map = InputMap::default(); + + input_map.insert(KeyCode::F11, GlobalAction::ToggleFullscreen); + input_map.insert(KeyCode::F10, GlobalAction::ToggleDiagnosticsOverlay); + + #[cfg(feature = "development")] + input_map.insert( + UserInput::chord([KeyCode::ControlLeft, KeyCode::P]), + GlobalAction::TogglePhysicsDebug, + ); // Insert the input map resource. app.insert_resource(input_map); diff --git a/game/src/input/plugin.rs b/game/src/input/plugin.rs index 93d3452..8de05f2 100644 --- a/game/src/input/plugin.rs +++ b/game/src/input/plugin.rs @@ -17,8 +17,14 @@ impl Plugin for InputPlugin { GameOverMenuAction::setup(app); // Add systems. - app.add_systems(Update, toggle_fullscreen); - app.add_systems(Update, toggle_diagnostics_overlay); - app.add_systems(Update, pause_on_losing_focus.in_set(GameplaySystems::Input)); + { + app.add_systems(Update, pause_on_losing_focus.in_set(GameplaySystems::Input)); + + app.add_systems(Update, toggle_fullscreen); + app.add_systems(Update, toggle_diagnostics_overlay); + + #[cfg(feature = "development")] + app.add_systems(Update, toggle_physics_debug); + } } } diff --git a/game/src/input/systems.rs b/game/src/input/systems.rs index fd03a44..50ada5b 100644 --- a/game/src/input/systems.rs +++ b/game/src/input/systems.rs @@ -1,6 +1,21 @@ use crate::prelude::*; +/// Pauses the game when the application loses it's focus. +pub fn pause_on_losing_focus( + mut window_focused_reader: EventReader, + general_settings: Res>, + mut next_game_state: ResMut>, +) { + for event in window_focused_reader.read() { + if !event.focused && general_settings.pause_on_losing_focus { + next_game_state.set(GameState::Paused); + break; + } + } +} + + /// Toggles the window mode between fullscreen and windowed. #[cfg(feature = "native")] pub fn toggle_fullscreen( @@ -70,16 +85,20 @@ pub fn toggle_diagnostics_overlay( } -/// Pauses the game when the application loses it's focus. -pub fn pause_on_losing_focus( - mut window_focused_reader: EventReader, - general_settings: Res>, - mut next_game_state: ResMut>, +/// Toggles physics debug. +#[cfg(feature = "development")] +pub fn toggle_physics_debug( + global_action_state: Res>, + mut general_settings: ResMut>, + mut physics_debug_config: ResMut, ) { - for event in window_focused_reader.read() { - if !event.focused && general_settings.pause_on_losing_focus { - next_game_state.set(GameState::Paused); - break; - } + if global_action_state.just_pressed(GlobalAction::TogglePhysicsDebug) { + general_settings + .update(|general_settings| { + general_settings.debug_physics = !general_settings.debug_physics; + }) + .ok(); + + physics_debug_config.enabled = general_settings.debug_physics; } } diff --git a/game/src/inventory/resources.rs b/game/src/inventory/resources.rs index 75ef28b..7ca1d03 100644 --- a/game/src/inventory/resources.rs +++ b/game/src/inventory/resources.rs @@ -4,21 +4,21 @@ use crate::prelude::*; /// Container for the items in the inventory. #[derive(Debug)] pub struct ItemInstance { - pub item: Box, + pub item: Box, pub entity: Option, } impl ItemInstance { /// Creates a new item instance. - pub fn new(item: impl Item) -> ItemInstance { + pub fn new(item: impl IItem) -> ItemInstance { ItemInstance { item: Box::new(item), entity: None } } } impl Deref for ItemInstance { - type Target = Box; + type Target = Box; - fn deref(&self) -> &Box { + fn deref(&self) -> &Box { &self.item } } diff --git a/game/src/inventory/systems.rs b/game/src/inventory/systems.rs index b293246..1dd4e63 100644 --- a/game/src/inventory/systems.rs +++ b/game/src/inventory/systems.rs @@ -16,15 +16,25 @@ pub fn acquire_release_items(world: &mut World) { } } for item_to_release in items_to_release { - item_to_release.release(world, item_to_release.entity); + if let Some(entity) = item_to_release.entity { + item_to_release.release(world, entity); + } } let mut new_items = Vec::with_capacity(items_to_acquire.len()); for mut item_to_acquire in items_to_acquire { - item_to_acquire.entity = item_to_acquire.acquire(world); + item_to_acquire.entity = Some(item_to_acquire.acquire(world)); new_items.push(Arc::new(item_to_acquire)); } + if let Ok(player_entity) = world.query_filtered::>().get_single(world) { + for new_item in &new_items { + if let Some(new_item_entity) = new_item.entity { + world.entity_mut(player_entity).add_child(new_item_entity); + } + } + } + let mut inventory = world.resource_mut::(); inventory.items.extend(new_items); } @@ -38,6 +48,8 @@ pub fn clear_inventory(world: &mut World) { inventory.items_to_remove = Vec::new(); for item in std::mem::take(&mut inventory.items) { - item.release(world, item.entity); + if let Some(entity) = item.entity { + item.release(world, entity); + } } } diff --git a/game/src/items/interfaces.rs b/game/src/items/interfaces.rs index f327234..0d4de94 100644 --- a/game/src/items/interfaces.rs +++ b/game/src/items/interfaces.rs @@ -2,7 +2,7 @@ use crate::prelude::*; /// Interface for items. -pub trait Item: Debug + Send + Sync + 'static { +pub trait IItem: Debug + Send + Sync + 'static { /// Gets the unique identifier of the item. fn id(&self) -> SmolStr; /// Gets the name of the item. @@ -10,9 +10,8 @@ pub trait Item: Debug + Send + Sync + 'static { /// Instantiates the item to add it to the inventory. fn instantiate(&self) -> ItemInstance; - // Acquires the item. - fn acquire(&self, world: &mut World) -> Option; + fn acquire(&self, world: &mut World) -> Entity; // Releases the item. - fn release(&self, world: &mut World, entity: Option); + fn release(&self, world: &mut World, entity: Entity); } diff --git a/game/src/items/mod.rs b/game/src/items/mod.rs index 18bd2e2..7c8a2fe 100644 --- a/game/src/items/mod.rs +++ b/game/src/items/mod.rs @@ -1,2 +1,3 @@ pub mod interfaces; +pub mod plugin; pub mod registry; diff --git a/game/src/items/plugin.rs b/game/src/items/plugin.rs new file mode 100644 index 0000000..d124306 --- /dev/null +++ b/game/src/items/plugin.rs @@ -0,0 +1,11 @@ +use crate::prelude::*; + +/// Plugin for managing items. +pub struct ItemPlugin; + +impl Plugin for ItemPlugin { + fn build(&self, app: &mut App) { + // Initialize registry. + app.init_resource::(); + } +} diff --git a/game/src/items/registry.rs b/game/src/items/registry.rs index a70e670..87fbec2 100644 --- a/game/src/items/registry.rs +++ b/game/src/items/registry.rs @@ -1,59 +1,78 @@ use crate::prelude::*; -/// Registry for items. -pub static ITEM_REGISTRY: Mutex = Mutex::new(ItemRegistry::new()); /// Container for the item registry. -#[derive(Default, Deref, DerefMut, Resource)] -pub struct ItemRegistry(pub Vec); +#[derive(Debug, Default, Deref, Resource)] +pub struct ItemRegistry(Vec); impl ItemRegistry { - /// Creates a new item registry. - pub const fn new() -> ItemRegistry { - ItemRegistry(Vec::new()) + /// Registers an item to the item registry. + pub fn register(&mut self, item: impl IItem) -> &mut RegisteredItem { + let item_id = item.id(); + let item_index = match self.iter().position(|entry| entry.item.id() == item_id) { + Some(index) => { + log::warn!("tried to register {:?} to the item registry again", item_id); + index + }, + None => { + log::info!("registered {:?} to the item registry", item_id); + let index = self.len(); + self.0.push(ItemRegistryEntry::new(item)); + index + }, + }; + &mut self.0[item_index].item } } impl ItemRegistry { - /// Registers an item to the item registry. - pub fn register(&mut self, item: impl Item) -> &mut ItemRegistryEntry { - let id = item.id(); - if self.iter().any(|entry| entry.id() == id) { - log::warn!("tried to register {:?} to item registry again", item.id()); - } else { - log::info!("registered {:?} to item registry", item.name()); - self.push(ItemRegistryEntry::new(item)); - } - self.iter_mut().find(|entry| entry.id() == id).unwrap() + /// Gets the number of items in the item registry. + pub fn number_of_items(&self) -> usize { + self.0.len() } } -/// Container for item registry entries. + +/// Container for the entries of the iem registry. #[derive(Debug)] pub struct ItemRegistryEntry { - pub item: Arc, - pub tags: SmallVec<[SmolStr; 3]>, + pub item: RegisteredItem, } impl ItemRegistryEntry { - /// Create a new entry for an item. - pub fn new(item: I) -> ItemRegistryEntry { - ItemRegistryEntry { item: Arc::new(item), tags: SmallVec::new() } + /// Creates a new item registry entry. + pub fn new(item: impl IItem) -> ItemRegistryEntry { + ItemRegistryEntry { item: RegisteredItem::new(item) } } } -impl ItemRegistryEntry { - /// Add a tag to the item. - pub fn add_tag(&mut self, tag: impl ToString) -> &mut ItemRegistryEntry { + +/// Container for registered items. +#[derive(Debug)] +pub struct RegisteredItem { + pub item: Arc, + pub tags: SmallVec<[SmolStr; 3]>, +} + +impl RegisteredItem { + /// Creates a new registered item. + pub fn new(item: impl IItem) -> RegisteredItem { + RegisteredItem { item: Arc::new(item), tags: SmallVec::new() } + } +} + +impl RegisteredItem { + /// Adds a tag to the item. + pub fn add_tag(&mut self, tag: impl ToString) -> &mut RegisteredItem { self.tags.push(tag.to_string().into()); self } } -impl Deref for ItemRegistryEntry { - type Target = Arc; +impl Deref for RegisteredItem { + type Target = Arc; - fn deref(&self) -> &Arc { + fn deref(&self) -> &Arc { &self.item } } diff --git a/game/src/lib.rs b/game/src/lib.rs index 5ea48bf..c36f157 100644 --- a/game/src/lib.rs +++ b/game/src/lib.rs @@ -1,7 +1,3 @@ -#![allow(clippy::derivable_impls)] -#![allow(clippy::new_without_default)] -#![allow(clippy::type_complexity)] - pub mod camera; pub mod combat; pub mod configuration; diff --git a/game/src/map/components.rs b/game/src/map/components.rs index 43104da..6a87325 100644 --- a/game/src/map/components.rs +++ b/game/src/map/components.rs @@ -4,3 +4,8 @@ use crate::prelude::*; /// Tag component for the map. #[derive(Component, Debug, Reflect)] pub struct Map; + + +/// Tag component for the invisible walls around the map. +#[derive(Component, Debug, Reflect)] +pub struct MapBound; diff --git a/game/src/map/constants.rs b/game/src/map/constants.rs new file mode 100644 index 0000000..73a2cc2 --- /dev/null +++ b/game/src/map/constants.rs @@ -0,0 +1,2 @@ +/// Thickness of map bounds. +pub const BOUND_THICKNESS: f32 = 50.00; diff --git a/game/src/map/mod.rs b/game/src/map/mod.rs index 81726da..9a51235 100644 --- a/game/src/map/mod.rs +++ b/game/src/map/mod.rs @@ -1,4 +1,6 @@ pub mod components; +pub mod constants; pub mod plugin; pub mod resources; pub mod systems; +pub mod utils; diff --git a/game/src/map/plugin.rs b/game/src/map/plugin.rs index 0adccb1..ae8504a 100644 --- a/game/src/map/plugin.rs +++ b/game/src/map/plugin.rs @@ -13,8 +13,10 @@ impl Plugin for MapPlugin { // Register components. app.register_type::(); + app.register_type::(); // Add systems. + app.add_systems(OnExit(GameState::Loading), spawn_map_bounds); app.add_systems(OnEnter(GameState::Won), despawn_map); app.add_systems(OnEnter(GameState::Over), despawn_map); app.add_systems(OnEnter(GameState::Restart), despawn_map.in_set(RestartSystems::Map)); diff --git a/game/src/map/systems.rs b/game/src/map/systems.rs index dc68e4f..f78db95 100644 --- a/game/src/map/systems.rs +++ b/game/src/map/systems.rs @@ -1,4 +1,62 @@ -use crate::prelude::*; +use crate::{ + map::constants::*, + prelude::*, +}; + + +/// Spawns the bounds of the map. +pub fn spawn_map_bounds( + mut commands: Commands, + map_bounds: Res, + map_query: Query>, +) { + let mut map = match map_query.get_single() { + Ok(map_entity) => commands.entity(map_entity), + Err(_) => return, + }; + map.with_children(|parent| { + let layers = CollisionLayers::new( + [Layer::MapBound], + [Layer::Player, Layer::Enemy, Layer::Projectile], + ); + + let x_length = (map_bounds.x_max - map_bounds.x_min) + (4.00 * BOUND_THICKNESS); + let y_length = map_bounds.y_max - map_bounds.y_min; + + parent.spawn(( + Name::new("Left Bound"), + MapBound, + RigidBody::Static, + Collider::cuboid(2.00 * BOUND_THICKNESS, y_length), + layers, + Position(Vector::X * (map_bounds.x_min - BOUND_THICKNESS)), + )); + parent.spawn(( + Name::new("Top Bound"), + MapBound, + RigidBody::Static, + Collider::cuboid(x_length, 2.00 * BOUND_THICKNESS), + layers, + Position(Vector::Y * (map_bounds.y_max + BOUND_THICKNESS)), + )); + parent.spawn(( + Name::new("Right Bound"), + MapBound, + RigidBody::Static, + Collider::cuboid(2.00 * BOUND_THICKNESS, y_length), + layers, + Position(Vector::X * (map_bounds.x_max + BOUND_THICKNESS)), + )); + parent.spawn(( + Name::new("Bottom Bound"), + MapBound, + RigidBody::Static, + Collider::cuboid(x_length, 2.00 * BOUND_THICKNESS), + layers, + Position(Vector::Y * (map_bounds.y_min - BOUND_THICKNESS)), + )); + }); +} /// Despawns the map. diff --git a/game/src/map/utils.rs b/game/src/map/utils.rs new file mode 100644 index 0000000..895dd27 --- /dev/null +++ b/game/src/map/utils.rs @@ -0,0 +1,18 @@ +use crate::prelude::*; + + +/// Finds the first obstacle from `position` along `direction` within `distance` units. +pub fn find_obstacle( + spatial_query: &SpatialQuery, + position: &Position, + direction: &Vec2, + distance: f32, +) -> Option { + spatial_query.cast_ray( + position.xy(), + *direction, + distance, + false, + SpatialQueryFilter::new().with_masks([Layer::MapObstacle]), + ) +} diff --git a/game/src/mode/conditions.rs b/game/src/mode/conditions.rs index d4904ef..897d66a 100644 --- a/game/src/mode/conditions.rs +++ b/game/src/mode/conditions.rs @@ -2,6 +2,6 @@ use crate::prelude::*; /// Run condition for game mode. -pub fn in_game_mode(mode: Option>>) -> bool { +pub fn in_game_mode(mode: Option>>) -> bool { mode.is_some() } diff --git a/game/src/mode/interfaces.rs b/game/src/mode/interfaces.rs index ef5af88..7c7ddb7 100644 --- a/game/src/mode/interfaces.rs +++ b/game/src/mode/interfaces.rs @@ -2,7 +2,7 @@ use crate::prelude::*; /// Interface for game modes. -pub trait Mode: Debug + Send + Sync + 'static { +pub trait IGameMode: Debug + Send + Sync + 'static { /// Gets the unique identifier of the game mode. fn id(&self) -> SmolStr; /// Gets the name of the game mode. diff --git a/game/src/mode/plugin.rs b/game/src/mode/plugin.rs index 5a57ee5..13f2d53 100644 --- a/game/src/mode/plugin.rs +++ b/game/src/mode/plugin.rs @@ -11,14 +11,17 @@ impl Plugin for ModePlugin { // Register resources. app.register_type::(); + // Initialize registry. + app.init_resource::(); + // Add systems. app.add_systems( OnEnter(GameState::Initialization), - initialize_game_mode.in_set(InitializationSystems::GameMode), + initialize_game_mode.in_set(InitializationSystems::First), ); app.add_systems( OnEnter(GameState::Restart), - restart_game_mode.in_set(RestartSystems::GameMode), + restart_game_mode.in_set(RestartSystems::Last), ); app.add_systems(OnExit(AppState::Game), deinitialize_game_mode); } diff --git a/game/src/mode/registry.rs b/game/src/mode/registry.rs index d50e1d1..7278bd5 100644 --- a/game/src/mode/registry.rs +++ b/game/src/mode/registry.rs @@ -1,35 +1,88 @@ use crate::prelude::*; -/// Registry for game modes. -pub static GAME_MODE_REGISTRY: Mutex = Mutex::new(GameModeRegistry::new()); -/// Container for game mode registry. -#[derive(Default, Deref, DerefMut, Resource)] -pub struct GameModeRegistry(pub Vec>); +/// Container for the game mode registry. +#[derive(Debug, Default, Deref, Resource)] +pub struct GameModeRegistry(Vec); impl GameModeRegistry { - /// Creates a new game mode registry. - pub const fn new() -> GameModeRegistry { - GameModeRegistry(Vec::new()) + /// Registers a game mode to the game mode registry. + pub fn register(&mut self, game_mode: impl IGameMode) -> &mut RegisteredGameMode { + let game_mode_id = game_mode.id(); + let game_mode_index = match self + .iter() + .position(|entry| entry.game_mode.id() == game_mode_id) + { + Some(index) => { + log::warn!("tried to register {:?} to the game mode registry again", game_mode_id); + index + }, + None => { + log::info!("registered {:?} to the game mode registry", game_mode_id); + let index = self.len(); + self.0.push(GameModeRegistryEntry::new(game_mode)); + index + }, + }; + &mut self.0[game_mode_index].game_mode } } impl GameModeRegistry { - /// Registers a game mode to game mode registry. - pub fn register(&mut self, game_mode: impl Mode) { - if self.iter().any(|existing_entry| existing_entry.id() == game_mode.id()) { - log::warn!("tried to register {:?} to game mode registry again", game_mode.name()); - } else { - log::info!("registered {:?} to game mode registry", game_mode.name()); - self.push(Arc::new(game_mode)); - } + /// Gets the number of game modes in the game mode registry. + pub fn number_of_game_modes(&self) -> usize { + self.len() } } impl Index for GameModeRegistry { - type Output = Arc; + type Output = GameModeRegistryEntry; - fn index(&self, index: SelectedGameModeIndex) -> &Arc { - &self.deref()[index.0] + fn index(&self, game_mode_index: SelectedGameModeIndex) -> &GameModeRegistryEntry { + &self.0[*game_mode_index] + } +} + + +/// Container for the entries of the game mode registry. +#[derive(Debug)] +pub struct GameModeRegistryEntry { + pub game_mode: RegisteredGameMode, +} + +impl GameModeRegistryEntry { + /// Creates a new game mode registry entry. + pub fn new(game_mode: impl IGameMode) -> GameModeRegistryEntry { + GameModeRegistryEntry { game_mode: RegisteredGameMode::new(game_mode) } + } +} + +impl Deref for GameModeRegistryEntry { + type Target = RegisteredGameMode; + + fn deref(&self) -> &RegisteredGameMode { + &self.game_mode + } +} + + +/// Container for registered game modes. +#[derive(Debug)] +pub struct RegisteredGameMode { + pub game_mode: Arc, +} + +impl RegisteredGameMode { + /// Creates a new registered game mode. + pub fn new(game_mode: impl IGameMode) -> RegisteredGameMode { + RegisteredGameMode { game_mode: Arc::new(game_mode) } + } +} + +impl Deref for RegisteredGameMode { + type Target = Arc; + + fn deref(&self) -> &Arc { + &self.game_mode } } diff --git a/game/src/mode/resources.rs b/game/src/mode/resources.rs index 85702ee..9279125 100644 --- a/game/src/mode/resources.rs +++ b/game/src/mode/resources.rs @@ -6,11 +6,6 @@ use crate::prelude::*; pub struct SelectedGameModeIndex(pub usize); -/// Resource for the selected game mode. -#[derive(Clone, Debug, Deref, Resource)] -pub struct SelectedGameMode(pub Arc); - - /// Resource for the current game mode. #[derive(Debug, Default, Deref, Reflect, Resource)] -pub struct GameMode(pub M); +pub struct GameMode(pub M); diff --git a/game/src/mode/systems.rs b/game/src/mode/systems.rs index 239d0d7..abc1577 100644 --- a/game/src/mode/systems.rs +++ b/game/src/mode/systems.rs @@ -3,24 +3,23 @@ use crate::prelude::*; /// Initializes the selected game mode. pub fn initialize_game_mode(world: &mut World) { - let game_mode_registry = GAME_MODE_REGISTRY.lock().unwrap(); - let selection = world.resource::(); - game_mode_registry[*selection].initialize(world); + let game_mode_registry = world.resource::(); + let selected_game_mode_index = world.resource::(); + game_mode_registry[*selected_game_mode_index].clone().initialize(world); } /// Restarts up the selected game mode. pub fn restart_game_mode(world: &mut World) { - let game_mode_registry = GAME_MODE_REGISTRY.lock().unwrap(); - let selection = world.resource::(); - game_mode_registry[*selection].deinitialize(world); + let game_mode_registry = world.resource::(); + let selected_game_mode_index = world.resource::(); + game_mode_registry[*selected_game_mode_index].clone().deinitialize(world); } /// Deinitializes the selected game mode. pub fn deinitialize_game_mode(world: &mut World) { - let game_mode_registry = GAME_MODE_REGISTRY.lock().unwrap(); - let selection = world.resource::(); - game_mode_registry[*selection].deinitialize(world); + let game_mode_registry = world.resource::(); + let selected_game_mode_index = world.resource::(); + game_mode_registry[*selected_game_mode_index].clone().deinitialize(world); world.remove_resource::(); - world.remove_resource::(); } diff --git a/game/src/physics/layers.rs b/game/src/physics/layers.rs index c5c1111..2e5ffdb 100644 --- a/game/src/physics/layers.rs +++ b/game/src/physics/layers.rs @@ -4,8 +4,26 @@ use crate::prelude::*; /// Physics layers to differentiate collisions. #[derive(Debug, PhysicsLayer, Reflect)] pub enum Layer { + /// Layer for the invisible bounds of the map. MapBound, + /// Layer for the obstacles in the map. + MapObstacle, + + /// Layer for the player. Player, - PlayerHitBox, + /// Layer for the enemies. Enemy, + + /// Layer for the hit box of the player. + PlayerHitBox, + /// Layer for damaging the player. + DamagePlayer, + + /// Layer for the hit box of the enemies. + EnemyHitBox, + /// Layer for damaging the enemies. + DamageEnemies, + + /// Layer for projectiles. + Projectile, } diff --git a/game/src/physics/plugin.rs b/game/src/physics/plugin.rs index e557819..7a56b8a 100644 --- a/game/src/physics/plugin.rs +++ b/game/src/physics/plugin.rs @@ -13,7 +13,19 @@ impl Plugin for PhysicsPlugin { // Setup physics. app.insert_resource(Gravity::ZERO); - app.add_plugins(XpbdPlugin::new(PostUpdate)); + app.add_plugins(XpbdPlugin::default()); + + // Setup physics debug in development mode. + #[cfg(feature = "development")] + { + let general_settings = app.world.resource::>(); + app.insert_resource(PhysicsDebugConfig { + enabled: general_settings.debug_physics, + ..default() + }); + + app.add_plugins(PhysicsDebugPlugin::default()); + } // Pause physics in startup. app.world.resource_mut::>().pause(); diff --git a/game/src/player/components.rs b/game/src/player/components.rs index 06a2c43..656fc67 100644 --- a/game/src/player/components.rs +++ b/game/src/player/components.rs @@ -2,36 +2,81 @@ use crate::prelude::*; /// Tag component for the player. -#[derive(Component, Debug, Reflect)] +#[derive(Component, Debug, Default, Reflect)] pub struct Player; /// Tag component for the hit box of the player. -#[derive(Component, Debug, Reflect)] +#[derive(Component, Debug, Default, Reflect)] pub struct PlayerHitBox; +impl PlayerHitBox { + pub fn bundle(collider: Collider) -> impl Bundle { + ( + // Tags + Name::new("Hit Box"), + PlayerHitBox, + // Physics + collider, + CollisionLayers::new([Layer::PlayerHitBox], [Layer::DamagePlayer]), + Sensor, + ) + } +} + + +/// Tag component for entities that apply damage to the player on contact. +#[derive(Component, Debug, Default, Reflect)] +pub struct DamagePlayerOnContact; + /// Bundle for players. -#[derive(Bundle)] -pub struct PlayerBundle { - // Tags - pub name: Name, - pub tag: Player, - // Properties - pub damage: Damage, - pub health: Health, - pub speed: Speed, - // Combat - pub remaining_health: RemainingHealth, - // Physics - pub body: RigidBody, - pub restitution: Restitution, - pub position: Position, - pub collider: Collider, - pub velocity: LinearVelocity, - pub layers: CollisionLayers, - // Texture +#[derive(Bundle, TypedBuilder)] +pub struct PlayerBundle { + pub player: P, pub mesh: MaterialMesh2dBundle, - // Input + #[builder(setter(transform = + |input_map: InputMap| { + InputManagerBundle:: { action_state: ActionState::default(), input_map } + } + ))] pub input: InputManagerBundle, } + +impl PlayerBundle

{ + /// Spawns the player. + pub fn spawn<'w, 's, 'a>( + self, + commands: &'a mut Commands<'w, 's>, + ) -> EntityCommands<'w, 's, 'a> { + let name = format!("Player [{}]", self.player.name()); + let health = self.player.health(); + let speed = self.player.speed(); + let collider = self.player.collider(); + + let mut player = commands.spawn(( + // Tags + Name::new(name), + Player, + // Player + self, + health, + speed, + // Combat + RemainingHealth(*health), + // Physics + RigidBody::Dynamic, + LinearVelocity::ZERO, + Restitution::PERFECTLY_INELASTIC, + LockedAxes::ROTATION_LOCKED, + collider.clone(), + CollisionLayers::new([Layer::Player], [Layer::MapBound]), + )); + + player.with_children(|parent| { + parent.spawn(PlayerHitBox::bundle(collider)); + }); + + player + } +} diff --git a/game/src/player/constants.rs b/game/src/player/constants.rs index a3e5f2c..5cee579 100644 --- a/game/src/player/constants.rs +++ b/game/src/player/constants.rs @@ -1,22 +1,15 @@ use crate::prelude::*; -/// Size of the player. -pub const PLAYER_SIZE: f32 = 20.00; +/// Base health of players. +pub const BASE_HEALTH: f32 = 10.00; +/// Base speed of players. +pub const BASE_SPEED: f32 = 200.00; -/// Initial damage of the player. -pub const INITIAL_PLAYER_DAMAGE: f32 = 5.00; -/// Initial health of the player. -pub const INITIAL_PLAYER_HEALTH: f32 = 10.00; +/// Base duration of dashing of players. +pub const BASE_DASH_DURATION: Duration = Duration::from_millis(75); -/// Initial speed of the player. -pub const INITIAL_PLAYER_SPEED: f32 = 200.00; - - -/// Initial duration of dashing of the player. -pub const INITIAL_DASH_DURATION: Duration = Duration::from_millis(75); - -/// Initial cooldown of dashing of the player. -pub const INITIAL_DASH_COOLDOWN: Duration = Duration::from_millis(1000); +/// Base cooldown of dashing of players. +pub const BASE_DASH_COOLDOWN: Duration = Duration::from_millis(1000); diff --git a/game/src/player/interfaces.rs b/game/src/player/interfaces.rs index b97ee02..f5abddf 100644 --- a/game/src/player/interfaces.rs +++ b/game/src/player/interfaces.rs @@ -1,8 +1,11 @@ -use crate::prelude::*; +use crate::{ + player::constants::*, + prelude::*, +}; /// Interface for mythologies. -pub trait Mythology: Any + Debug + Send + Sync + 'static { +pub trait IMythology: Any + Debug + Send + Sync + 'static { /// Gets the unique identifier of the mythology. fn id(&self) -> SmolStr; /// Gets the name of the mythology. @@ -11,12 +14,23 @@ pub trait Mythology: Any + Debug + Send + Sync + 'static { /// Interface for players. -pub trait Playable: Debug + Send + Sync + 'static { +pub trait IPlayer: Debug + Send + Sync + 'static { /// Gets the unique identifier of the player. fn id(&self) -> SmolStr; /// Gets the name of the player. fn name(&self) -> SmolStr; + /// Gets the health of the player. + fn health(&self) -> Health { + Health(BASE_HEALTH) + } + /// Gets the speed of the player. + fn speed(&self) -> Speed { + Speed(BASE_SPEED) + } + + /// Gets the collider of the player. + fn collider(&self) -> Collider; /// Spawns the player. fn spawn(&self, world: &mut World); } diff --git a/game/src/player/plugin.rs b/game/src/player/plugin.rs index 402acb7..a5d7afe 100644 --- a/game/src/player/plugin.rs +++ b/game/src/player/plugin.rs @@ -10,8 +10,13 @@ impl Plugin for PlayerPlugin { fn build(&self, app: &mut App) { // Register components. app.register_type::(); + app.register_type::(); + app.register_type::(); app.register_type::(); + // Initialize registry. + app.init_resource::(); + // Add systems. { app.add_systems( diff --git a/game/src/player/registry.rs b/game/src/player/registry.rs index 432b928..1f43f03 100644 --- a/game/src/player/registry.rs +++ b/game/src/player/registry.rs @@ -1,61 +1,84 @@ use crate::prelude::*; -/// Registry for players. -pub static PLAYER_REGISTRY: Mutex = Mutex::new(PlayerRegistry::new()); -/// Container for player registry. -#[derive(Default, Deref, DerefMut, Resource)] -pub struct PlayerRegistry(pub Vec<(Arc, Vec>)>); +/// Container for the player registry. +#[derive(Debug, Default, Deref, Resource)] +pub struct PlayerRegistry(Vec); impl PlayerRegistry { - /// Creates a new player registry. - pub const fn new() -> PlayerRegistry { - PlayerRegistry(Vec::new()) - } -} + /// Registers a player to the player registry. + pub fn register( + &mut self, + mythology: impl IMythology, + player: impl IPlayer, + ) -> &mut RegisteredPlayer { + let mythology_id = mythology.id(); + let mythology_index = self + .iter() + .position(|entry| entry.mythology.id() == mythology_id) + .unwrap_or_else(|| { + let index = self.len(); + self.0.push(PlayerRegistryEntry::new(mythology)); + index + }); -impl PlayerRegistry { - /// Registers a player to player registry. - pub fn register(&mut self, mythology: impl Mythology, player: impl Playable) { - let mythology_name = mythology.name(); - let player_name = player.name(); + let players = &mut self.0[mythology_index].players; - let entries = match self + let player_id = player.id(); + let player_index = match players .iter_mut() - .find(|(existing_mythology, _)| existing_mythology.id() == mythology.id()) + .position(|registered_player| registered_player.id() == player_id) { - Some((_, entries)) => entries, + Some(index) => { + log::warn!( + "tried to register {:?} from {:?} mythology to the player registry again", + player_id, + mythology_id, + ); + index + }, None => { - self.push((Arc::new(mythology), Vec::new())); - &mut self.last_mut().unwrap().1 + log::info!( + "registered {:?} from {:?} mythology to the player registry", + player_id, + mythology_id, + ); + let index = players.len(); + players.push(RegisteredPlayer::new(player)); + index }, }; - if entries.iter().any(|existing_player| existing_player.id() == player.id()) { - log::warn!( - "tried to register {:?} from {:?} mythology to player registry again", - player_name, - mythology_name, - ); - } else { - log::info!( - "registered {:?} from {:?} mythology to player registry", - player_name, - mythology_name, - ); - entries.push(Arc::new(player)); - } + &mut players[player_index] + } +} + +impl PlayerRegistry { + /// Gets the number of mythologies in the player registry. + pub fn number_of_mythologies(&self) -> usize { + self.len() + } + + /// Gets the number of players in the player registry. + pub fn number_of_players(&self) -> usize { + self.iter().map(|entry| entry.players.len()).sum() } } impl PlayerRegistry { - /// Finds the player from it's id. - pub fn find(&self, id: impl AsRef) -> Option { - let id = id.as_ref(); - for (mythology_index, (_, players)) in self.iter().enumerate() { - for (player_index, player) in players.iter().enumerate() { - if player.id() == id { - return Some(SelectedPlayerIndex { mythology_index, player_index }); + /// Tries to find the player in the player registry by id. + pub fn find_player( + &self, + player_id: impl AsRef, + ) -> Option<(SelectedMythologyIndex, SelectedPlayerIndex)> { + let player_id = player_id.as_ref(); + for (mythology_index, entry) in self.iter().enumerate() { + for (player_index, player) in entry.players.iter().enumerate() { + if player.id() == player_id { + return Some(( + SelectedMythologyIndex(mythology_index), + SelectedPlayerIndex(player_index), + )); } } } @@ -63,19 +86,85 @@ impl PlayerRegistry { } } -impl Index for PlayerRegistry { - type Output = (Arc, Vec>); +impl Index for PlayerRegistry { + type Output = PlayerRegistryEntry; + + fn index(&self, mythology_index: SelectedMythologyIndex) -> &PlayerRegistryEntry { + &self.0[*mythology_index] + } +} + + +/// Container for the entries of the player registry. +#[derive(Debug)] +pub struct PlayerRegistryEntry { + pub mythology: RegisteredMythology, + pub players: Vec, +} + +impl PlayerRegistryEntry { + /// Creates a new player registry entry. + pub fn new(mythology: impl IMythology) -> PlayerRegistryEntry { + PlayerRegistryEntry { mythology: RegisteredMythology::new(mythology), players: Vec::new() } + } +} + +impl Deref for PlayerRegistryEntry { + type Target = RegisteredMythology; + + fn deref(&self) -> &RegisteredMythology { + &self.mythology + } +} + +impl Index for PlayerRegistryEntry { + type Output = RegisteredPlayer; + + fn index(&self, player_index: SelectedPlayerIndex) -> &RegisteredPlayer { + &self.players[*player_index] + } +} + + +/// Container for registered mythologies. +#[derive(Debug)] +pub struct RegisteredMythology { + pub mythology: Arc, +} + +impl RegisteredMythology { + /// Creates a new registered mythology. + pub fn new(mythology: impl IMythology) -> RegisteredMythology { + RegisteredMythology { mythology: Arc::new(mythology) } + } +} + +impl Deref for RegisteredMythology { + type Target = Arc; + + fn deref(&self) -> &Arc { + &self.mythology + } +} + + +/// Container for registered players. +#[derive(Debug)] +pub struct RegisteredPlayer { + pub player: Arc, +} - fn index(&self, index: usize) -> &(Arc, Vec>) { - &self.deref()[index] +impl RegisteredPlayer { + /// Creates a new registered player. + pub fn new(player: impl IPlayer) -> RegisteredPlayer { + RegisteredPlayer { player: Arc::new(player) } } } -impl Index for PlayerRegistry { - type Output = Arc; +impl Deref for RegisteredPlayer { + type Target = Arc; - fn index(&self, index: SelectedPlayerIndex) -> &Arc { - let (_, players) = &self.deref()[index.mythology_index]; - &players.deref()[index.player_index] + fn deref(&self) -> &Arc { + &self.player } } diff --git a/game/src/player/resources.rs b/game/src/player/resources.rs index d9a8a41..0500453 100644 --- a/game/src/player/resources.rs +++ b/game/src/player/resources.rs @@ -1,13 +1,11 @@ use crate::prelude::*; +/// Resource for the index of the selected mythology. +#[derive(Clone, Copy, Debug, Deref, Reflect, Resource)] +pub struct SelectedMythologyIndex(pub usize); + + /// Resource for the index of the selected player. -#[derive(Clone, Copy, Debug, Reflect, Resource)] -pub struct SelectedPlayerIndex { - pub mythology_index: usize, - pub player_index: usize, -} - -/// Resource for the selected player. -#[derive(Clone, Debug, Deref, Resource)] -pub struct SelectedPlayer(pub Arc); +#[derive(Clone, Copy, Debug, Deref, Reflect, Resource)] +pub struct SelectedPlayerIndex(pub usize); diff --git a/game/src/player/systems.rs b/game/src/player/systems.rs index f6a3cf3..4f17c65 100644 --- a/game/src/player/systems.rs +++ b/game/src/player/systems.rs @@ -6,9 +6,10 @@ use crate::{ /// Spawns the player. pub fn spawn_player(world: &mut World) { - let player_registry = PLAYER_REGISTRY.lock().unwrap(); - let selection = world.resource::(); - player_registry[*selection].spawn(world); + let player_registry = world.resource::(); + let selected_mythology_index = world.resource::(); + let selected_player_index = world.resource::(); + player_registry[*selected_mythology_index][*selected_player_index].clone().spawn(world); } /// Despawns the player. @@ -94,8 +95,8 @@ pub fn dash( return; } commands.entity(entity).insert(( - Dashing { timer: Timer::new(INITIAL_DASH_DURATION, TimerMode::Once) }, - Cooldown::::new(Timer::new(INITIAL_DASH_COOLDOWN, TimerMode::Once)), + Dashing { timer: Timer::new(BASE_DASH_DURATION, TimerMode::Once) }, + Cooldown::::new(BASE_DASH_COOLDOWN), )); } } @@ -116,6 +117,6 @@ pub fn pause( /// Clears player selection. pub fn clear_player_selection(mut commands: Commands) { + commands.remove_resource::(); commands.remove_resource::(); - commands.remove_resource::(); } diff --git a/game/src/plugin.rs b/game/src/plugin.rs index c6f0cc8..72e7f46 100644 --- a/game/src/plugin.rs +++ b/game/src/plugin.rs @@ -6,6 +6,7 @@ use crate::{ enemy::plugin::EnemyPlugin, input::plugin::InputPlugin, inventory::plugin::InventoryPlugin, + items::plugin::ItemPlugin, map::plugin::MapPlugin, mode::plugin::ModePlugin, movement::plugin::MovementPlugin, @@ -30,6 +31,7 @@ impl Plugin for MythmallowPlugin { app.add_plugins(UiPlugin); app.add_plugins(PhysicsPlugin); app.add_plugins(ModePlugin); + app.add_plugins(ItemPlugin); app.add_plugins(InventoryPlugin); app.add_plugins(MapPlugin); app.add_plugins(PropertyPlugin); diff --git a/game/src/prelude.rs b/game/src/prelude.rs index 536ac39..bcc2c7c 100644 --- a/game/src/prelude.rs +++ b/game/src/prelude.rs @@ -4,6 +4,7 @@ pub use crate::{ combat::components::*, configuration::resources::*, core::{ + depths::*, resources::*, sets::*, states::*, @@ -55,6 +56,13 @@ pub use crate::{ }, }; +pub mod utils { + pub use crate::{ + combat::utils as combat, + map::utils as map, + }; +} + #[doc(inline)] pub use { bevy::transform::TransformSystem, @@ -67,7 +75,10 @@ pub use { }, ecs::{ self as bevy_ecs, - system::RunSystemOnce, + system::{ + EntityCommands, + RunSystemOnce, + }, }, input::mouse::MouseMotion, log::{ @@ -108,6 +119,7 @@ pub use { Any, TypeId, }, + cmp::Ordering, fmt::{ self, Debug, @@ -128,6 +140,7 @@ pub use { }, strum::IntoEnumIterator, strum_macros::EnumIter, + typed_builder::TypedBuilder, }; #[cfg(feature = "native")] diff --git a/game/src/status_effect/components.rs b/game/src/status_effect/components.rs index bdc3208..d9f607a 100644 --- a/game/src/status_effect/components.rs +++ b/game/src/status_effect/components.rs @@ -14,8 +14,8 @@ pub struct Cooldown { impl Cooldown { /// Creates a cooldown. - pub fn new(timer: Timer) -> Cooldown { - Cooldown { timer, phantom: PhantomData } + pub fn new(duration: Duration) -> Cooldown { + Cooldown { timer: Timer::new(duration, TimerMode::Once), phantom: PhantomData } } } diff --git a/game/src/ui/enemy_selection_screen/systems.rs b/game/src/ui/enemy_selection_screen/systems.rs index 263ddf4..8cda187 100644 --- a/game/src/ui/enemy_selection_screen/systems.rs +++ b/game/src/ui/enemy_selection_screen/systems.rs @@ -2,26 +2,18 @@ use crate::prelude::*; /// Spawns the enemy selection screen. -pub fn spawn_enemy_selection_screen(mut commands: Commands) { - let enemy_registry = ENEMY_REGISTRY.lock().unwrap(); - +pub fn spawn_enemy_selection_screen(mut commands: Commands, enemy_registry: Res) { if enemy_registry.is_empty() { - drop(enemy_registry); // TODO: Replace panic with a proper error communicated through the UI. panic!("no enemy packs are available"); } if enemy_registry.len() == 1 { - let selection_index = SelectedEnemyPackIndex(0); - let selection = SelectedEnemyPack(enemy_registry[selection_index].clone()); - - commands.insert_resource(selection_index); - commands.insert_resource(selection); - + let selected_enemy_pack_index = SelectedEnemyPackIndex(0); + commands.insert_resource(selected_enemy_pack_index); return; } - drop(enemy_registry); // TODO: Add support for multiple enemy packs and a nice enemy selection screen. panic!("multiple enemy packs are not supported at the moment") } @@ -52,20 +44,16 @@ pub fn select_enemy_pack_when_starting_in_game( mut commands: Commands, args: ResMut, mut rng: ResMut>, + enemy_registry: Res, ) { - let enemy_registry = ENEMY_REGISTRY.lock().unwrap(); match &args.start_in_game_mode { Some(specified_enemy_pack_id) => { - for (index, (enemy_pack, _)) in enemy_registry.iter().enumerate() { - if enemy_pack.id() == specified_enemy_pack_id { - log::info!("selected manually specified {:?} enemies", enemy_pack.id()); - - let selection_index = SelectedEnemyPackIndex(index); - let selection = SelectedEnemyPack(enemy_registry[selection_index].clone()); - - commands.insert_resource(selection_index); - commands.insert_resource(selection); + for (index, entry) in enemy_registry.iter().enumerate() { + if entry.pack.id() == specified_enemy_pack_id { + log::info!("selected manually specified {:?} enemies", entry.pack.id()); + let selected_enemy_pack_index = SelectedEnemyPackIndex(index); + commands.insert_resource(selected_enemy_pack_index); return; } } @@ -83,14 +71,13 @@ pub fn select_enemy_pack_when_starting_in_game( return; } - let selection_index = + let selected_enemy_pack_index = SelectedEnemyPackIndex((0..enemy_registry.len()).choose(rng.deref_mut()).unwrap()); - let selection = SelectedEnemyPack(enemy_registry[selection_index].clone()); - log::info!("randomly selected {:?} enemies", enemy_registry[selection_index].0.name()); + let selected_enemy_pack = &enemy_registry[selected_enemy_pack_index].pack; + log::info!("randomly selected {:?} enemies", selected_enemy_pack.name()); - commands.insert_resource(selection_index); - commands.insert_resource(selection); + commands.insert_resource(selected_enemy_pack_index); }, } } diff --git a/game/src/ui/game_mode_selection_screen/systems.rs b/game/src/ui/game_mode_selection_screen/systems.rs index 4914dc5..6c468bb 100644 --- a/game/src/ui/game_mode_selection_screen/systems.rs +++ b/game/src/ui/game_mode_selection_screen/systems.rs @@ -2,26 +2,21 @@ use crate::prelude::*; /// Spawns the game mode selection screen. -pub fn spawn_game_mode_selection_screen(mut commands: Commands) { - let game_mode_registry = GAME_MODE_REGISTRY.lock().unwrap(); - +pub fn spawn_game_mode_selection_screen( + mut commands: Commands, + game_mode_registry: Res, +) { if game_mode_registry.is_empty() { - drop(game_mode_registry); // TODO: Replace panic with a proper error communicated through the UI. panic!("no game modes are available"); } if game_mode_registry.len() == 1 { - let selection_index = SelectedGameModeIndex(0); - let selection = SelectedGameMode(Arc::clone(&game_mode_registry[selection_index])); - - commands.insert_resource(selection_index); - commands.insert_resource(selection); - + let selected_game_mode_index = SelectedGameModeIndex(0); + commands.insert_resource(selected_game_mode_index); return; } - drop(game_mode_registry); // TODO: Add support for multiple game modes and a nice game mode selection screen. panic!("multiple game modes are not supported at the moment") } @@ -48,21 +43,16 @@ pub fn select_game_mode_when_starting_in_game( mut commands: Commands, args: ResMut, mut rng: ResMut>, + game_mode_registry: Res, ) { - let game_mode_registry = GAME_MODE_REGISTRY.lock().unwrap(); match &args.start_in_game_mode { Some(specified_game_mode_id) => { - for (index, game_mode) in game_mode_registry.iter().enumerate() { - if game_mode.id() == specified_game_mode_id { - log::info!("selected manually specified {:?} game mode", game_mode.id()); - - let selection_index = SelectedGameModeIndex(index); - let selection = - SelectedGameMode(Arc::clone(&game_mode_registry[selection_index])); - - commands.insert_resource(selection_index); - commands.insert_resource(selection); + for (game_mode_index, entry) in game_mode_registry.iter().enumerate() { + if entry.game_mode.id() == specified_game_mode_id { + log::info!("selected manually specified {:?} game mode", entry.game_mode.id()); + let selected_game_mode_index = SelectedGameModeIndex(game_mode_index); + commands.insert_resource(selected_game_mode_index); return; } } @@ -80,15 +70,14 @@ pub fn select_game_mode_when_starting_in_game( return; } - let selection_index = SelectedGameModeIndex( + let selected_game_mode_index = SelectedGameModeIndex( (0..game_mode_registry.len()).choose(rng.deref_mut()).unwrap(), ); - let selection = SelectedGameMode(Arc::clone(&game_mode_registry[selection_index])); - log::info!("randomly selected {:?} game mode", selection.name()); + let selected_game_mode = &game_mode_registry[selected_game_mode_index]; + log::info!("randomly selected {:?} game mode", selected_game_mode.name()); - commands.insert_resource(selection_index); - commands.insert_resource(selection); + commands.insert_resource(selected_game_mode_index); }, } } diff --git a/game/src/ui/game_over_menu/systems.rs b/game/src/ui/game_over_menu/systems.rs index 954dfeb..1a66dac 100644 --- a/game/src/ui/game_over_menu/systems.rs +++ b/game/src/ui/game_over_menu/systems.rs @@ -29,7 +29,7 @@ pub fn spawn_game_over_menu( Name::new("Play Again Button"), GameOverMenuPlayAgainButton, Widget::default().selected(), - WidgetSelected::new(), + WidgetSelected::now(), ), &button_style, button_colors, @@ -48,7 +48,7 @@ pub fn spawn_game_over_menu( Name::new("Retry Button"), GameOverMenuRetryButton, Widget::default().selected(), - WidgetSelected::new(), + WidgetSelected::now(), ), &button_style, button_colors, @@ -185,9 +185,9 @@ pub fn navigation( if (go_up || go_down) && !(go_up && go_down) { if go_down { - commands.entity(down_widget.0).insert(WidgetSelected::new()); + commands.entity(down_widget.0).insert(WidgetSelected::now()); } else { - commands.entity(up_widget.0).insert(WidgetSelected::new()); + commands.entity(up_widget.0).insert(WidgetSelected::now()); } } } diff --git a/game/src/ui/main_menu/systems.rs b/game/src/ui/main_menu/systems.rs index 29ee60d..0a39785 100644 --- a/game/src/ui/main_menu/systems.rs +++ b/game/src/ui/main_menu/systems.rs @@ -24,7 +24,7 @@ pub fn spawn_main_menu( Name::new("Play Button"), MainMenuPlayButton, Widget::default().selected(), - WidgetSelected::new(), + WidgetSelected::now(), ), &button_style, button_colors, @@ -119,9 +119,9 @@ pub fn navigation( if (go_up || go_down) && !(go_up && go_down) { if go_down { - commands.entity(down_widget.0).insert(WidgetSelected::new()); + commands.entity(down_widget.0).insert(WidgetSelected::now()); } else { - commands.entity(up_widget.0).insert(WidgetSelected::new()); + commands.entity(up_widget.0).insert(WidgetSelected::now()); } } } diff --git a/game/src/ui/pause_menu/systems.rs b/game/src/ui/pause_menu/systems.rs index f943f9d..ad3b932 100644 --- a/game/src/ui/pause_menu/systems.rs +++ b/game/src/ui/pause_menu/systems.rs @@ -24,7 +24,7 @@ pub fn spawn_pause_menu( Name::new("Resume Button"), PauseMenuResumeButton, Widget::default().selected(), - WidgetSelected::new(), + WidgetSelected::now(), ), &button_style, button_colors, @@ -141,9 +141,9 @@ pub fn navigation( if (go_up || go_down) && !(go_up && go_down) { if go_down { - commands.entity(down_widget.0).insert(WidgetSelected::new()); + commands.entity(down_widget.0).insert(WidgetSelected::now()); } else { - commands.entity(up_widget.0).insert(WidgetSelected::new()); + commands.entity(up_widget.0).insert(WidgetSelected::now()); } } } diff --git a/game/src/ui/player_selection_screen/components.rs b/game/src/ui/player_selection_screen/components.rs index 57241f4..c02e287 100644 --- a/game/src/ui/player_selection_screen/components.rs +++ b/game/src/ui/player_selection_screen/components.rs @@ -9,6 +9,8 @@ pub struct PlayerSelectionScreen; /// Tag component for player buttons in the player selection screen. #[derive(Component, Debug, Reflect)] pub struct PlayerSelectionScreenPlayerButton { + // Index of the mythology the button represents. + pub mythology_index: SelectedMythologyIndex, // Index of the player the button represents. pub player_index: SelectedPlayerIndex, } diff --git a/game/src/ui/player_selection_screen/plugin.rs b/game/src/ui/player_selection_screen/plugin.rs index a6f70bb..4b16b35 100644 --- a/game/src/ui/player_selection_screen/plugin.rs +++ b/game/src/ui/player_selection_screen/plugin.rs @@ -23,9 +23,12 @@ impl Plugin for PlayerSelectionScreenPlugin { ); app.add_systems( PostUpdate, - player_selected - .in_set(PlayerSelectionScreenSystems) - .run_if(|player_index: Option>| player_index.is_some()), + player_selected.in_set(PlayerSelectionScreenSystems).run_if( + |selected_mythology_index: Option>, + selected_player_index: Option>| { + selected_mythology_index.is_some() && selected_player_index.is_some() + }, + ), ); app.add_systems(OnExit(AppState::PlayerSelectionScreen), despawn_player_selection_screen); diff --git a/game/src/ui/player_selection_screen/systems.rs b/game/src/ui/player_selection_screen/systems.rs index a93f29a..4881825 100644 --- a/game/src/ui/player_selection_screen/systems.rs +++ b/game/src/ui/player_selection_screen/systems.rs @@ -12,6 +12,7 @@ pub fn spawn_player_selection_screen( mut commands: Commands, asset_server: Res, player_selection_screen_action_input_map: Res>, + player_registry: Res, ) { let button_style = styles::button(); let button_colors = WidgetColors::button(); @@ -21,20 +22,21 @@ pub fn spawn_player_selection_screen( let mut entities = Vec::new(); let mut first = true; - let player_registry = PLAYER_REGISTRY.lock().unwrap(); - for (mythology_index, (_mythology, players)) in player_registry.iter().enumerate() { + for (mythology_index, entry) in player_registry.iter().enumerate() { // TODO: Group player buttons by mythology. - for (player_index, player) in players.iter().enumerate() { - let player_index = SelectedPlayerIndex { mythology_index, player_index }; + for (player_index, player) in entry.players.iter().enumerate() { + let mythology_index = SelectedMythologyIndex(mythology_index); + let player_index = SelectedPlayerIndex(player_index); + let player_button = if first { first = false; Widget::button( &mut commands, ( Name::new(format!("{} Button", player.name())), - PlayerSelectionScreenPlayerButton { player_index }, + PlayerSelectionScreenPlayerButton { mythology_index, player_index }, Widget::default().selected(), - WidgetSelected::new(), + WidgetSelected::now(), ), &button_style, button_colors, @@ -47,7 +49,7 @@ pub fn spawn_player_selection_screen( &mut commands, ( Name::new(format!("{} Button", player.name())), - PlayerSelectionScreenPlayerButton { player_index }, + PlayerSelectionScreenPlayerButton { mythology_index, player_index }, Widget::default(), ), &button_style, @@ -162,9 +164,9 @@ pub fn navigation( if (go_up || go_down) && !(go_up && go_down) { if go_down { - commands.entity(down_widget.0).insert(WidgetSelected::new()); + commands.entity(down_widget.0).insert(WidgetSelected::now()); } else { - commands.entity(up_widget.0).insert(WidgetSelected::new()); + commands.entity(up_widget.0).insert(WidgetSelected::now()); } } } @@ -195,6 +197,7 @@ pub fn player_button_interaction( ) { for (mut button, metadata) in &mut player_button_query { button.on_click(|| { + commands.insert_resource(metadata.mythology_index); commands.insert_resource(metadata.player_index); }); } @@ -212,22 +215,25 @@ pub fn select_player_when_starting_in_game( mut commands: Commands, args: ResMut, mut rng: ResMut>, + player_registry: Res, ) { - let player_registry = PLAYER_REGISTRY.lock().unwrap(); match &args.start_in_game_player { Some(specified_player_id) => { - if let Some(selection_index) = player_registry.find(specified_player_id) { + if let Some((mythology_index, player_index)) = + player_registry.find_player(specified_player_id) + { + let selected_mythology = &player_registry[mythology_index]; + let selected_player = &selected_mythology[player_index]; + log::info!( "selected manually specified {:?} first found in {:?} mythology \ as the player", - player_registry[selection_index].name(), - player_registry[selection_index.mythology_index].0.name() + selected_player.id(), + selected_mythology.id() ); - let selection = SelectedPlayer(Arc::clone(&player_registry[selection_index])); - - commands.insert_resource(selection_index); - commands.insert_resource(selection); + commands.insert_resource(mythology_index); + commands.insert_resource(player_index); } else { log::error!( "couldn't select \ @@ -244,22 +250,25 @@ pub fn select_player_when_starting_in_game( } let number_of_mythologies = player_registry.len(); - let mythology_index = (0..number_of_mythologies).choose(rng.deref_mut()).unwrap(); + let mythology_index = + SelectedMythologyIndex((0..number_of_mythologies).choose(rng.deref_mut()).unwrap()); - let number_of_players_in_mythology = player_registry[mythology_index].1.len(); - let player_index = (0..number_of_players_in_mythology).choose(rng.deref_mut()).unwrap(); + let number_of_players_in_mythology = player_registry[mythology_index].players.len(); + let player_index = SelectedPlayerIndex( + (0..number_of_players_in_mythology).choose(rng.deref_mut()).unwrap(), + ); - let selection_index = SelectedPlayerIndex { mythology_index, player_index }; - let selection = SelectedPlayer(Arc::clone(&player_registry[selection_index])); + let selected_mythology = player_registry[mythology_index].clone(); + let selected_player = player_registry[mythology_index][player_index].clone(); log::info!( "randomly selected {:?} from {:?} mythology as the player", - player_registry[selection_index].name(), - player_registry[mythology_index].0.name() + selected_player.id(), + selected_mythology.id() ); - commands.insert_resource(selection_index); - commands.insert_resource(selection); + commands.insert_resource(mythology_index); + commands.insert_resource(player_index); }, } } diff --git a/game/src/ui/widget/components.rs b/game/src/ui/widget/components.rs index 27be592..3759555 100644 --- a/game/src/ui/widget/components.rs +++ b/game/src/ui/widget/components.rs @@ -148,7 +148,7 @@ pub struct WidgetSelected { impl WidgetSelected { /// Creates a new widget selected component at this instant. - pub fn new() -> WidgetSelected { + pub fn now() -> WidgetSelected { WidgetSelected { at: Instant::now() } } } diff --git a/game/src/ui/widget/systems.rs b/game/src/ui/widget/systems.rs index 7853bd2..5703c46 100644 --- a/game/src/ui/widget/systems.rs +++ b/game/src/ui/widget/systems.rs @@ -11,7 +11,7 @@ pub fn manage_widget_selected_on_mouse_wiggle( let mut hovered = false; for (entity, interaction) in &widget_query { if *interaction == Interaction::Hovered { - commands.entity(entity).insert(WidgetSelected::new()); + commands.entity(entity).insert(WidgetSelected::now()); hovered = true; break; } @@ -19,7 +19,7 @@ pub fn manage_widget_selected_on_mouse_wiggle( if !hovered { for (entity, interaction) in &widget_query { if *interaction == Interaction::Pressed { - commands.entity(entity).insert(WidgetSelected::new()); + commands.entity(entity).insert(WidgetSelected::now()); break; } } @@ -77,7 +77,7 @@ pub fn update_widget_state_on_user_interactions( widget.clicked = true; } - commands.entity(entity).insert(WidgetSelected::new()); + commands.entity(entity).insert(WidgetSelected::now()); widget.is_selected = true; widget.is_hovered = true; diff --git a/items/greek/Cargo.toml b/items/greek/Cargo.toml index 4c7496b..011f1cd 100644 --- a/items/greek/Cargo.toml +++ b/items/greek/Cargo.toml @@ -5,3 +5,6 @@ edition = "2021" [dependencies] mythmallow = { path = "../../game", package = "mythmallow-game" } + +[lints] +workspace = true diff --git a/items/greek/src/bident_of_hades.rs b/items/greek/src/bident_of_hades.rs index 57cde0c..408b2d6 100644 --- a/items/greek/src/bident_of_hades.rs +++ b/items/greek/src/bident_of_hades.rs @@ -4,10 +4,11 @@ use { }; /// Tag component for the item "Bident of Hades". -#[derive(Clone, Component, Debug, Reflect)] +#[derive(Clone, Component, Debug, Default, Reflect)] +#[reflect(Component)] pub struct BidentOfHades; -impl Item for BidentOfHades { +impl IItem for BidentOfHades { fn id(&self) -> SmolStr { "bident-of-hades".into() } @@ -20,12 +21,12 @@ impl Item for BidentOfHades { ItemInstance::new(self.clone()) } - fn acquire(&self, _world: &mut World) -> Option { - None + fn acquire(&self, world: &mut World) -> Entity { + world.run_system_once_with(self.clone(), acquire) } - fn release(&self, _world: &mut World, entity: Option) { - assert_eq!(entity, None); + fn release(&self, world: &mut World, entity: Entity) { + world.run_system_once_with(entity, release); } } @@ -35,11 +36,31 @@ pub struct BidentOfHadesPlugin; impl Plugin for BidentOfHadesPlugin { fn build(&self, app: &mut App) { // Register the item. - let mut item_registry = ITEM_REGISTRY.lock().unwrap(); + let mut item_registry = app.world.resource_mut::(); item_registry.register(BidentOfHades).add_tag(GREEK_ITEM_TAG); - drop(item_registry); - // Register resources. + // Register components. app.register_type::(); } } + +/// Acquires the item. +pub fn acquire( + In(item): In, + mut commands: Commands, + inventory: Res, +) -> Entity { + commands + .spawn(( + Name::new(format!("Item {} [{}]", inventory.items.len(), item.name().to_string())), + item, + )) + .id() +} + +/// Releases the item. +pub fn release(In(entity): In, mut commands: Commands) { + if let Some(entity) = commands.get_entity(entity) { + entity.despawn_recursive(); + } +} diff --git a/items/greek/src/bow_of_artemis.rs b/items/greek/src/bow_of_artemis.rs index 7da05b7..24581b4 100644 --- a/items/greek/src/bow_of_artemis.rs +++ b/items/greek/src/bow_of_artemis.rs @@ -3,11 +3,36 @@ use { mythmallow::prelude::*, }; +/// Size of the item. +pub const SIZE: f32 = 5.00; + +/// Color of the item. +pub const COLOR: Color = Color::BLUE; + +/// Base range of the item. +pub const BASE_RANGE: f32 = 250.00; + +/// Base damage of the item. +pub const BASE_DAMAGE: Damage = Damage(5.00); + +/// Base cooldown duration of the attacks with the item. +pub const BASE_ATTACK_COOLDOWN: Duration = Duration::from_millis(600); + +/// Size of the projectiles of the item. +pub const PROJECTILE_SIZE: f32 = 3.00; + +/// Color of the projectiles of the item. +pub const PROJECTILE_COLOR: Color = Color::DARK_GRAY; + +/// Base speed for the projectiles of the item. +pub const BASE_PROJECTILE_SPEED: f32 = 200.00; + /// Tag component for the item "Bow of Artemis". -#[derive(Clone, Component, Debug, Reflect)] +#[derive(Clone, Component, Debug, Default, Reflect)] +#[reflect(Component)] pub struct BowOfArtemis; -impl Item for BowOfArtemis { +impl IItem for BowOfArtemis { fn id(&self) -> SmolStr { "bow-of-artemis".into() } @@ -20,12 +45,12 @@ impl Item for BowOfArtemis { ItemInstance::new(self.clone()) } - fn acquire(&self, _world: &mut World) -> Option { - None + fn acquire(&self, world: &mut World) -> Entity { + world.run_system_once_with(self.clone(), acquire) } - fn release(&self, _world: &mut World, entity: Option) { - assert!(entity.is_none()); + fn release(&self, world: &mut World, entity: Entity) { + world.run_system_once_with(entity, release); } } @@ -35,11 +60,101 @@ pub struct BowOfArtemisPlugin; impl Plugin for BowOfArtemisPlugin { fn build(&self, app: &mut App) { // Register the item. - let mut item_registry = ITEM_REGISTRY.lock().unwrap(); + let mut item_registry = app.world.resource_mut::(); item_registry.register(BowOfArtemis).add_tag(GREEK_ITEM_TAG); - drop(item_registry); // Register resources. app.register_type::(); + + // Add systems. + app.add_systems(Update, attack.in_set(GameplaySystems::Item)); + } +} + +/// Acquires the item. +pub fn acquire( + In(item): In, + mut commands: Commands, + inventory: Res, + mut meshes: ResMut>, + mut materials: ResMut>, +) -> Entity { + commands + .spawn(( + Name::new(format!("Item {} [{}]", inventory.items.len(), item.name().to_string())), + item, + MaterialMesh2dBundle { + mesh: meshes.add(shape::Circle::new(SIZE).into()).into(), + material: materials.add(ColorMaterial::from(COLOR)), + transform: Transform::from_translation(Vec3::new(0.00, 0.00, Depth::Item.z())), + ..default() + }, + )) + .id() +} + +/// Releases the item. +pub fn release(In(entity): In, mut commands: Commands) { + if let Some(entity) = commands.get_entity(entity) { + entity.despawn_recursive(); + } +} + +/// Attacks with the item. +pub fn attack( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + item_query: Query<(Entity, &GlobalTransform), (With, Without>)>, + enemy_hit_box_query: Query<&Position, With>, + spatial_query: SpatialQuery, +) { + let base_attack_area = Collider::ball(BASE_RANGE); + for (item_entity, &item_transform) in item_query.iter() { + let item_position = Position(item_transform.translation().xy()); + let enemies_in_range = utils::combat::find_enemies_in_range_sorted_by_distance( + &spatial_query, + &item_position, + &base_attack_area, + &enemy_hit_box_query, + ); + + for (_, enemy_position, enemy_distance) in enemies_in_range { + let enemy_direction = (enemy_position.xy() - item_position.xy()).normalize(); + + let obstacle_between_item_and_enemy = utils::map::find_obstacle( + &spatial_query, + &item_position, + &enemy_direction, + enemy_distance, + ); + if obstacle_between_item_and_enemy.is_some() { + continue; + } + + let projectile_entity = ProjectileBundle::builder() + .mesh(MaterialMesh2dBundle { + mesh: meshes.add(shape::Circle::new(PROJECTILE_SIZE).into()).into(), + material: materials.add(ColorMaterial::from(PROJECTILE_COLOR)), + transform: Transform::from_translation( + item_position.extend(Depth::Projectile.z()), + ), + ..default() + }) + .collider(Collider::ball(PROJECTILE_SIZE)) + .position(item_position) + .velocity(LinearVelocity(enemy_direction * BASE_PROJECTILE_SPEED)) + .damage(BASE_DAMAGE) + .build() + .spawn_toward_enemies(&mut commands) + .id(); + + commands + .entity(item_entity) + .add_child(projectile_entity) + .insert(Cooldown::::new(BASE_ATTACK_COOLDOWN)); + + break; + } } } diff --git a/items/greek/src/constants.rs b/items/greek/src/constants.rs index 2392cea..5d91c5a 100644 --- a/items/greek/src/constants.rs +++ b/items/greek/src/constants.rs @@ -1,2 +1,2 @@ -/// Tag for greek items. +/// Tag for items from "Greek" mythology. pub const GREEK_ITEM_TAG: &str = "greek"; diff --git a/items/greek/src/plugin.rs b/items/greek/src/plugin.rs index eb6d063..c32e4fd 100644 --- a/items/greek/src/plugin.rs +++ b/items/greek/src/plugin.rs @@ -6,7 +6,7 @@ use { mythmallow::prelude::*, }; -/// Plugin for managing the items from Greek mythology. +/// Plugin for managing the items from "Greek" mythology. pub struct GreekItemsPlugin; impl Plugin for GreekItemsPlugin { diff --git a/modes/survival/Cargo.toml b/modes/survival/Cargo.toml index 15190ee..116e7ba 100644 --- a/modes/survival/Cargo.toml +++ b/modes/survival/Cargo.toml @@ -5,3 +5,6 @@ edition = "2021" [dependencies] mythmallow = { path = "../../game", package = "mythmallow-game" } + +[lints] +workspace = true diff --git a/modes/survival/src/constants.rs b/modes/survival/src/constants.rs index a8f2420..f9dd3cd 100644 --- a/modes/survival/src/constants.rs +++ b/modes/survival/src/constants.rs @@ -1,13 +1,26 @@ -/// Size of the map. Value of 10 means the map will be a 10x10 grid of squares. -pub const MAP_SIZE: i32 = 10; +use mythmallow::prelude::*; +/// Size of the grid. Value of 10 means the map will be a 10x10 grid of squares. +pub const GRID_SIZE: i32 = 10; + /// Amount of space between grid elements. pub const GRID_SPACING: f32 = 50.0; /// Thickness of grid elements. pub const GRID_WIDTH: f32 = 2.0; +/// Color of the grid. +pub const GRID_COLOR: Color = Color::rgb(0.27, 0.27, 0.27); + + +/// Size of the map. +pub const MAP_SIZE: f32 = (GRID_SIZE as f32) * GRID_SPACING; -/// Bound of the map in terms of world coordinates. -pub const MAP_BOUND: f32 = (MAP_SIZE as f32) * GRID_SPACING / 2.00; +/// Bounds of the map. +pub const MAP_BOUNDS: MapBounds = MapBounds { + x_min: -(MAP_SIZE / 2.00), + x_max: (MAP_SIZE / 2.00), + y_min: -(MAP_SIZE / 2.00), + y_max: (MAP_SIZE / 2.00), +}; diff --git a/modes/survival/src/mode.rs b/modes/survival/src/mode.rs index 616a94c..aad5bb0 100644 --- a/modes/survival/src/mode.rs +++ b/modes/survival/src/mode.rs @@ -3,11 +3,12 @@ use mythmallow::{ prelude::*, }; -/// Survival game mode. +/// Resource for "Survival" game mode. #[derive(Debug, Default, Reflect, Resource)] +#[reflect(Resource)] pub struct Survival; -impl Mode for Survival { +impl IGameMode for Survival { fn id(&self) -> SmolStr { "survival".into() } @@ -17,9 +18,15 @@ impl Mode for Survival { } fn default_enemy_spawn_pattern(&self, world: &World) -> EnemySpawnPattern { - let (_, enemies) = world.resource::().deref(); - let enemy = - enemies.iter().find(|enemy| enemy.has_tag(MELEE_ENEMY_TAG)).map(|enemy| enemy.deref()); + let enemy_registry = world.resource::(); + + let selected_enemy_pack_index = world.resource::(); + let enemies_in_selected_pack = &enemy_registry[*selected_enemy_pack_index].enemies; + + let enemy = enemies_in_selected_pack + .iter() + .find(|enemy| enemy.has_tag(MELEE_ENEMY_TAG)) + .map(|enemy| enemy.deref()); let mut spawns = Vec::new(); if let Some(enemy) = enemy { diff --git a/modes/survival/src/plugin.rs b/modes/survival/src/plugin.rs index 2beb168..67fcb70 100644 --- a/modes/survival/src/plugin.rs +++ b/modes/survival/src/plugin.rs @@ -6,15 +6,14 @@ use { mythmallow::prelude::*, }; -/// Plugin for managing the "Survival" game mode. +/// Plugin for managing "Survival" game mode. pub struct SurvivalModePlugin; impl Plugin for SurvivalModePlugin { fn build(&self, app: &mut App) { // Register the game mode. - let mut game_mode_registry = GAME_MODE_REGISTRY.lock().unwrap(); + let mut game_mode_registry = app.world.resource_mut::(); game_mode_registry.register(Survival); - drop(game_mode_registry); // Register resources. app.register_type::(); @@ -40,7 +39,7 @@ impl Plugin for SurvivalModePlugin { // Add gameplay systems. app.add_systems( - Update, + PreUpdate, tick.in_set(GameplaySystems::GameMode).run_if(in_game_mode::), ); diff --git a/modes/survival/src/resources.rs b/modes/survival/src/resources.rs index d9f9840..668ea49 100644 --- a/modes/survival/src/resources.rs +++ b/modes/survival/src/resources.rs @@ -3,6 +3,7 @@ use mythmallow::prelude::*; /// Resource for the current wave. #[derive(Debug, Deref, DerefMut, Reflect, Resource)] +#[reflect(Resource)] pub struct CurrentWave(pub u8); impl Default for CurrentWave { @@ -14,4 +15,18 @@ impl Default for CurrentWave { /// Resource for the remaining time to complete the current wave. #[derive(Debug, Deref, DerefMut, Reflect, Resource)] +#[reflect(Resource)] pub struct WaveTimer(pub Timer); + +impl WaveTimer { + /// Creates a new wave timer. + pub fn new(duration: Duration) -> WaveTimer { + WaveTimer(Timer::new(duration, TimerMode::Once)) + } +} + +impl Default for WaveTimer { + fn default() -> WaveTimer { + WaveTimer(Timer::new(Duration::from_secs(60), TimerMode::Once)) + } +} diff --git a/modes/survival/src/systems.rs b/modes/survival/src/systems.rs index b1e4ce1..fe7b413 100644 --- a/modes/survival/src/systems.rs +++ b/modes/survival/src/systems.rs @@ -12,68 +12,29 @@ pub fn initialize(mut commands: Commands) { commands.insert_resource(CurrentWave(1)); } + /// Loads the current wave. pub fn load(mut commands: Commands) { - commands.insert_resource(WaveTimer(Timer::new(Duration::from_secs(5), TimerMode::Once))); + commands.insert_resource(WaveTimer::new(Duration::from_secs(10))); } - /// Spawns the map. pub fn spawn_map(mut commands: Commands) { - commands.insert_resource(MapBounds { - x_min: -MAP_BOUND, - x_max: MAP_BOUND, - y_min: -MAP_BOUND, - y_max: MAP_BOUND, - }); + commands.insert_resource(MAP_BOUNDS); commands.spawn((Name::new("Map"), Map, SpatialBundle::default())).with_children(|parent| { - // Define physics layer of the walls. - let layers = CollisionLayers::new([Layer::MapBound], [Layer::Player, Layer::Enemy]); - // Spawn left wall. - parent.spawn(( - Name::new("Left Wall"), - RigidBody::Static, - Collider::cuboid(50.0, MAP_BOUND * 2.0), - layers, - Position(Vector::NEG_X * (MAP_BOUND + 25.0)), - )); - // Spawn top wall. - parent.spawn(( - Name::new("Top Wall"), - RigidBody::Static, - Collider::cuboid(MAP_BOUND * 2.0, 50.0), - layers, - Position(Vector::Y * (MAP_BOUND + 25.0)), - )); - // Spawn right wall. - parent.spawn(( - Name::new("Right Wall"), - RigidBody::Static, - Collider::cuboid(50.0, MAP_BOUND * 2.0), - layers, - Position(Vector::X * (MAP_BOUND + 25.0)), - )); - // Spawn bottom wall. - parent.spawn(( - Name::new("Bottom Wall"), - RigidBody::Static, - Collider::cuboid(MAP_BOUND * 2.0, 50.0), - layers, - Position(Vector::NEG_Y * (MAP_BOUND + 25.0)), - )); // Spawn horizontal lines. - for i in 0..=MAP_SIZE { + for i in 0..=GRID_SIZE { parent.spawn(( Name::new(format!("Horizontal Line {}", i + 1)), SpriteBundle { transform: Transform::from_translation(Vec3::new( - 0.0, - (((MAP_SIZE as f32) / 2.0) - (i as f32)) * GRID_SPACING, - 0.0, + 0.00, + (((GRID_SIZE as f32) / 2.00) - (i as f32)) * GRID_SPACING, + Depth::Map.z(), )), sprite: Sprite { - color: Color::rgb(0.27, 0.27, 0.27), - custom_size: Some(Vec2::new(MAP_SIZE as f32 * GRID_SPACING, GRID_WIDTH)), + color: GRID_COLOR, + custom_size: Some(Vec2::new(GRID_SIZE as f32 * GRID_SPACING, GRID_WIDTH)), ..default() }, ..default() @@ -81,18 +42,18 @@ pub fn spawn_map(mut commands: Commands) { )); } // Spawn vertical lines. - for i in 0..=MAP_SIZE { + for i in 0..=GRID_SIZE { parent.spawn(( Name::new(format!("Vertical Line {}", i + 1)), SpriteBundle { transform: Transform::from_translation(Vec3::new( - ((i as f32) - ((MAP_SIZE as f32) / 2.0)) * GRID_SPACING, - 0.0, - 0.0, + ((i as f32) - ((GRID_SIZE as f32) / 2.00)) * GRID_SPACING, + 0.00, + Depth::Map.z(), )), sprite: Sprite { - color: Color::rgb(0.27, 0.27, 0.27), - custom_size: Some(Vec2::new(GRID_WIDTH, MAP_SIZE as f32 * GRID_SPACING)), + color: GRID_COLOR, + custom_size: Some(Vec2::new(GRID_WIDTH, GRID_SIZE as f32 * GRID_SPACING)), ..default() }, ..default() @@ -102,6 +63,7 @@ pub fn spawn_map(mut commands: Commands) { }); } + /// Ticks wave timer and wins the current wave when wave timer is finished. pub fn tick( time: Res