diff --git a/assets/elements/item/crate/crate.element.yaml b/assets/elements/item/crate/crate.element.yaml index 2cd0a22d81..d92cf44bf3 100644 --- a/assets/elements/item/crate/crate.element.yaml +++ b/assets/elements/item/crate/crate.element.yaml @@ -8,11 +8,20 @@ builtin: !Crate breaking_atlas: ./crate_breaking.atlas.yaml breaking_anim_frames: 25 breaking_anim_fps: 30 + crate_break_state_1: 0 + crate_break_state_2: 1 # TODO: Better break sound break_sound: ./fuse.ogg + break_sound_volume: 0.1 + + bounce_sound: ./land.ogg + bounce_sound_volume: 0.035 body_size: [36, 30] body_offset: [0, 0] grab_offset: [14, -2] break_timeout: 4 + bounciness: 0.5 + fin_anim: grab_2 + diff --git a/assets/elements/item/crate/land.ogg b/assets/elements/item/crate/land.ogg new file mode 100755 index 0000000000..4f36f5f669 Binary files /dev/null and b/assets/elements/item/crate/land.ogg differ diff --git a/core/src/elements.rs b/core/src/elements.rs index e73489bc9f..e697102b12 100644 --- a/core/src/elements.rs +++ b/core/src/elements.rs @@ -1,6 +1,7 @@ use crate::prelude::*; pub mod crab; +pub mod crate_item; pub mod decoration; pub mod grenade; pub mod kick_bomb; @@ -30,4 +31,5 @@ pub fn install(session: &mut GameSession) { kick_bomb::install(session); musket::install(session); stomp_boots::install(session); + crate_item::install(session); } diff --git a/core/src/elements/crate_item.rs b/core/src/elements/crate_item.rs new file mode 100644 index 0000000000..232f83138b --- /dev/null +++ b/core/src/elements/crate_item.rs @@ -0,0 +1,320 @@ +use crate::{physics::collisions::TileCollision, prelude::*}; + +pub fn install(session: &mut GameSession) { + session + .stages + .add_system_to_stage(CoreStage::PreUpdate, hydrate_crates) + .add_system_to_stage(CoreStage::PostUpdate, update_idle_crates) + .add_system_to_stage(CoreStage::PostUpdate, update_thrown_crates); +} + +#[derive(Clone, TypeUlid)] +#[ulid = "01GREP3MZXY4A14PQ8GRKS0RVY"] +struct IdleCrate { + spawner: Entity, +} + +#[derive(Clone, TypeUlid)] +#[ulid = "01GREP80RJSH9T9MWC88CG2G03"] +struct ThrownCrate { + spawner: Entity, + owner: Entity, + age: f32, + crate_break_state: u8, + was_colliding: bool, +} + +fn hydrate_crates( + game_meta: Res, + mut entities: ResMut, + mut hydrated: CompMut, + mut element_handles: CompMut, + element_assets: BevyAssets, + mut idle_crates: CompMut, + mut atlas_sprites: CompMut, + mut animated_sprites: CompMut, + mut bodies: CompMut, + mut transforms: CompMut, + mut items: CompMut, + mut respawn_points: CompMut, +) { + let mut not_hydrated_bitset = hydrated.bitset().clone(); + not_hydrated_bitset.bit_not(); + not_hydrated_bitset.bit_and(element_handles.bitset()); + + let spawners = entities + .iter_with_bitset(¬_hydrated_bitset) + .collect::>(); + + for spawner in spawners { + let transform = *transforms.get(spawner).unwrap(); + let element_handle = element_handles.get(spawner).unwrap(); + let Some(element_meta) = element_assets.get(&element_handle.get_bevy_handle()) else{ + continue; + }; + + let BuiltinElementKind::Crate{ + atlas, + body_size, + body_offset, + bounciness, + .. + } = &element_meta.builtin else{ + continue; + }; + + hydrated.insert(spawner, MapElementHydrated); + + let entity = entities.create(); + items.insert(entity, Item); + idle_crates.insert(entity, IdleCrate { spawner }); + atlas_sprites.insert(entity, AtlasSprite::new(atlas.clone())); + respawn_points.insert(entity, MapRespawnPoint(transform.translation)); + transforms.insert(entity, transform); + element_handles.insert(entity, element_handle.clone()); + hydrated.insert(entity, MapElementHydrated); + animated_sprites.insert(entity, default()); + bodies.insert( + entity, + KinematicBody { + size: *body_size, + offset: *body_offset, + has_mass: true, + can_rotate: false, + has_friction: true, + gravity: game_meta.physics.gravity, + bounciness: *bounciness, + ..default() + }, + ); + } +} + +fn update_idle_crates( + entities: Res, + element_handles: Comp, + element_assets: BevyAssets, + mut transforms: CompMut, + mut idle_crates: CompMut, + mut sprites: CompMut, + mut bodies: CompMut, + mut items_used: CompMut, + mut items_dropped: CompMut, + mut attachments: CompMut, + mut player_layers: CompMut, + player_inventories: PlayerInventories, + mut commands: Commands, + mut player_events: ResMut, +) { + for (entity, (crate_item, element_handle)) in + entities.iter_with((&mut idle_crates, &element_handles)) + { + let spawner = crate_item.spawner; + let Some(element_meta) = element_assets.get(&element_handle.get_bevy_handle()) else { + continue; + }; + + let BuiltinElementKind::Crate{ + grab_offset, + throw_velocity, + fin_anim, + .. + } = &element_meta.builtin else { + unreachable!(); + }; + + if let Some(inventory) = player_inventories + .iter() + .find_map(|x| x.filter(|x| x.inventory == entity)) + { + let player = inventory.player; + let player_layers = player_layers.get_mut(player).unwrap(); + player_layers.fin_anim = *fin_anim; + let body = bodies.get_mut(entity).unwrap(); + body.is_deactivated = true; + + attachments.insert( + entity, + PlayerBodyAttachment { + player, + offset: grab_offset.extend(0.1), + sync_animation: false, + }, + ); + + if items_used.get(entity).is_some() { + items_used.remove(entity); + player_events.set_inventory(player, None); + let [body, player_body] = bodies.get_many_mut([entity, player]).unwrap_many(); + let player_velocity = player_body.velocity; + let player_sprite = sprites.get_mut(entity).unwrap(); + let signum = if player_sprite.flip_x { + Vec2::new(-1.0, 1.0) + } else { + Vec2::ONE + }; + body.velocity = *throw_velocity * signum + player_velocity; + attachments.remove(entity); + body.is_deactivated = false; + commands.add( + move |mut idle: CompMut, mut thrown: CompMut| { + idle.remove(entity); + thrown.insert( + entity, + ThrownCrate { + spawner, + owner: player, + age: 0.0, + was_colliding: false, + crate_break_state: 0, + }, + ); + }, + ); + } + } + + if let Some(dropped) = items_dropped.get(entity).copied() { + let player = dropped.player; + items_dropped.remove(entity); + attachments.remove(entity); + + let player_translation = transforms.get(player).unwrap().translation; + let body = bodies.get_mut(entity).unwrap(); + + body.is_deactivated = false; + body.is_spawning = true; + let transform = transforms.get_mut(entity).unwrap(); + transform.translation = player_translation; + } + } +} + +fn update_thrown_crates( + entities: Res, + mut hydrated: CompMut, + element_assets: BevyAssets, + element_handles: Comp, + transforms: Comp, + mut thrown_crates: CompMut, + mut commands: Commands, + mut atlas_sprites: CompMut, + players: Comp, + collision_world: CollisionWorld, + mut player_events: ResMut, + mut bodies: CompMut, + mut audio_events: ResMut, +) { + for (entity, (mut thrown_crate, element_handle, transform, atlas_sprite, body)) in entities + .iter_with(( + &mut thrown_crates, + &element_handles, + &transforms, + &mut atlas_sprites, + &mut bodies, + )) + { + let Some(element_meta) = element_assets.get(&element_handle.get_bevy_handle()) else { + continue; + }; + + let BuiltinElementKind::Crate{ + breaking_anim_frames, + breaking_atlas, + breaking_anim_fps, + break_timeout, + break_sound, + break_sound_volume, + bounce_sound, + bounce_sound_volume, + crate_break_state_1, + crate_break_state_2, + .. + } = &element_meta.builtin else { + continue; + }; + + thrown_crate.age += 1.0 / crate::FPS; + + let colliding_with_tile = { + let collider = collision_world.get_collider(entity); + let width = collider.width + 2.0; + let height = collider.height + 2.0; + let pos = transform.translation.truncate(); + collision_world.collide_solids(pos, width, height) == TileCollision::SOLID + || collision_world.collide_solids(pos, width, height) == TileCollision::JUMP_THROUGH + }; + + if colliding_with_tile && !thrown_crate.was_colliding { + thrown_crate.was_colliding = true; + thrown_crate.crate_break_state += 1; + audio_events.play(bounce_sound.clone(), *bounce_sound_volume); + } else if !colliding_with_tile { + thrown_crate.was_colliding = false; + } + + match thrown_crate.crate_break_state { + 1 => { + atlas_sprite.atlas = breaking_atlas.clone(); + atlas_sprite.index = *crate_break_state_1; + } + 3 => { + atlas_sprite.index = *crate_break_state_2; + } + _ => {} + } + + let colliding_with_players = collision_world + .actor_collisions(entity) + .into_iter() + .filter(|x| *x != thrown_crate.owner && players.contains(*x)) + .collect::>(); + + for player_entity in &colliding_with_players { + player_events.kill(*player_entity); + } + + if !colliding_with_players.is_empty() + || thrown_crate.age >= *break_timeout + || thrown_crate.crate_break_state >= 4 + || body.velocity.length_squared() < 0.1 + { + hydrated.remove(thrown_crate.spawner); + + let breaking_anim_frames = *breaking_anim_frames; + let breaking_anim_fps = *breaking_anim_fps; + let atlas = breaking_atlas.clone(); + + audio_events.play(break_sound.clone(), *break_sound_volume); + commands.add( + move |mut entities: ResMut, + mut transforms: CompMut, + mut animated_sprites: CompMut, + mut lifetimes: CompMut, + mut atlas_sprites: CompMut| { + let pos = transforms.get(entity).unwrap(); + entities.kill(entity); + let breaking_anim_ent = entities.create(); + atlas_sprites.insert( + breaking_anim_ent, + AtlasSprite { + atlas: atlas.clone(), + ..default() + }, + ); + animated_sprites.insert( + breaking_anim_ent, + AnimatedSprite { + repeat: false, + fps: breaking_anim_fps, + frames: (1..breaking_anim_frames).collect(), + ..default() + }, + ); + lifetimes.insert(breaking_anim_ent, Lifetime::new(1.0)); + transforms.insert(breaking_anim_ent, *pos); + }, + ); + } + } +} diff --git a/core/src/metadata/element.rs b/core/src/metadata/element.rs index e0be23b96a..e5bd4793f9 100644 --- a/core/src/metadata/element.rs +++ b/core/src/metadata/element.rs @@ -135,6 +135,9 @@ pub enum BuiltinElementKind { breaking_anim_fps: f32, break_sound: Handle, + break_sound_volume: f32, + bounce_sound: Handle, + bounce_sound_volume: f32, throw_velocity: Vec2, @@ -143,6 +146,10 @@ pub enum BuiltinElementKind { grab_offset: Vec2, // How long to wait before despawning a thrown crate, if it hans't it anything yet. break_timeout: f32, + bounciness: f32, + fin_anim: Key, + crate_break_state_1: usize, + crate_break_state_2: usize, }, /// The mine item Mine {