Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(kick_bomb): Add spawn kickbomb command to allow spawning from gameplay code without interfering with map hydration #968

Merged
merged 1 commit into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 132 additions & 92 deletions src/core/elements/kick_bomb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,86 +52,142 @@ pub struct LitKickBomb {
kicks: u32,
}

fn hydrate(
game_meta: Root<GameMeta>,
mut items: CompMut<Item>,
mut item_throws: CompMut<ItemThrow>,
mut item_grabs: CompMut<ItemGrab>,
mut entities: ResMutInit<Entities>,
mut bodies: CompMut<KinematicBody>,
mut transforms: CompMut<Transform>,
mut idle_bombs: CompMut<IdleKickBomb>,
mut atlas_sprites: CompMut<AtlasSprite>,
assets: Res<AssetServer>,
mut hydrated: CompMut<MapElementHydrated>,
mut element_handles: CompMut<ElementHandle>,
mut animated_sprites: CompMut<AnimatedSprite>,
mut respawn_points: CompMut<DehydrateOutOfBounds>,
mut spawner_manager: SpawnerManager,
) {
let mut not_hydrated_bitset = hydrated.bitset().clone();
not_hydrated_bitset.bit_not();
not_hydrated_bitset.bit_and(element_handles.bitset());

let spawner_entities = entities
.iter_with_bitset(&not_hydrated_bitset)
.collect::<Vec<_>>();

for spawner_ent in spawner_entities {
let transform = *transforms.get(spawner_ent).unwrap();
let element_handle = *element_handles.get(spawner_ent).unwrap();
let element_meta = assets.get(element_handle.0);

if let Ok(KickBombMeta {
atlas,
fin_anim,
grab_offset,
body_diameter,
can_rotate,
bounciness,
throw_velocity,
angular_velocity,
..
}) = assets.get(element_meta.data).try_cast_ref()
{
hydrated.insert(spawner_ent, MapElementHydrated);

let entity = entities.create();
hydrated.insert(entity, MapElementHydrated);
element_handles.insert(entity, element_handle);
/// Component containing the kick bombs's metadata handle.
#[derive(Deref, DerefMut, HasSchema, Default, Clone)]
#[repr(C)]
pub struct KickBombHandle(pub Handle<KickBombMeta>);

/// Commands for KickBombs
#[derive(Clone, Debug)]
pub struct KickBombCommand;

impl KickBombCommand {
/// Command for spawning a kick bomb.
/// If entity is provided, components are added to this entity, otherwise command spawns the entity
///
/// `kick_bomb_handle` must cast to `Handle<KickBombMeta>` or `Handle<ElementMeta>` where [`ElementMeta`]
/// contains handle that casts to `Handle<KickBombMeta>`.
/// [`Handle::untyped`] should be used to convert to [`UntypedHandle`].
pub fn spawn_kick_bomb(
entity: Option<Entity>,
transform: Transform,
kick_bomb_meta_handle: UntypedHandle,
) -> StaticSystem<(), ()> {
(move |game_meta: Root<GameMeta>,
assets: Res<AssetServer>,
mut animated_sprites: CompMut<AnimatedSprite>,
mut atlas_sprites: CompMut<AtlasSprite>,
mut bodies: CompMut<KinematicBody>,
mut entities: ResMutInit<Entities>,
mut idle_bombs: CompMut<IdleKickBomb>,
mut items: CompMut<Item>,
mut item_throws: CompMut<ItemThrow>,
mut item_grabs: CompMut<ItemGrab>,
mut kick_bomb_handles: CompMut<KickBombHandle>,
mut transforms: CompMut<Transform>| {
// Unwrap entity or spawn if existing entity was not provided.
let entity = entity.unwrap_or_else(|| entities.create());

// Try to use handle as Handle<KickBombMeta>.
let kick_bomb_meta_handle =
match try_cast_meta_handle::<KickBombMeta>(kick_bomb_meta_handle, &assets) {
Ok(handle) => handle,
Err(err) => {
error!("KickBombCommand::spawn_kick_bomb() failed: {err}");
return;
}
};

let KickBombMeta {
atlas,
fin_anim,
grab_offset,
body_diameter,
can_rotate,
bounciness,
throw_velocity,
angular_velocity,
..
} = *assets.get(kick_bomb_meta_handle);
kick_bomb_handles.insert(entity, KickBombHandle(kick_bomb_meta_handle));
items.insert(entity, Item);
item_throws.insert(
entity,
ItemThrow::strength(*throw_velocity).with_spin(*angular_velocity),
ItemThrow::strength(throw_velocity).with_spin(angular_velocity),
);
item_grabs.insert(
entity,
ItemGrab {
fin_anim: *fin_anim,
fin_anim,
sync_animation: false,
grab_offset: *grab_offset,
grab_offset,
},
);
idle_bombs.insert(entity, IdleKickBomb);
atlas_sprites.insert(entity, AtlasSprite::new(*atlas));
respawn_points.insert(entity, DehydrateOutOfBounds(spawner_ent));
atlas_sprites.insert(entity, AtlasSprite::new(atlas));
transforms.insert(entity, transform);
animated_sprites.insert(entity, default());
bodies.insert(
entity,
KinematicBody {
shape: ColliderShape::Circle {
diameter: *body_diameter,
diameter: body_diameter,
},
gravity: game_meta.core.physics.gravity,
has_mass: true,
has_friction: true,
can_rotate: *can_rotate,
bounciness: *bounciness,
can_rotate,
bounciness,
..default()
},
);
spawner_manager.create_spawner(spawner_ent, vec![entity])
})
.system()
}
}

fn hydrate(
assets: Res<AssetServer>,
mut entities: ResMutInit<Entities>,
transforms: Comp<Transform>,
mut hydrated: CompMut<MapElementHydrated>,
mut element_handles: CompMut<ElementHandle>,
mut respawn_points: CompMut<DehydrateOutOfBounds>,
mut spawner_manager: SpawnerManager,
mut commands: Commands,
) {
let mut not_hydrated_bitset = hydrated.bitset().clone();
not_hydrated_bitset.bit_not();
not_hydrated_bitset.bit_and(element_handles.bitset());

let spawner_entities = entities
.iter_with_bitset(&not_hydrated_bitset)
.collect::<Vec<_>>();

for spawner_ent in spawner_entities {
let transform = *transforms.get(spawner_ent).unwrap();
let element_handle = *element_handles.get(spawner_ent).unwrap();

hydrated.insert(spawner_ent, MapElementHydrated);

let entity = entities.create();
hydrated.insert(entity, MapElementHydrated);
element_handles.insert(entity, element_handle);
respawn_points.insert(entity, DehydrateOutOfBounds(spawner_ent));
spawner_manager.create_spawner(spawner_ent, vec![entity]);

// Check if spawner element handle is for kick bomb
let element_meta = assets.get(element_handle.0);
if assets
.get(element_meta.data)
.try_cast_ref::<KickBombMeta>()
.is_ok()
{
commands.add(KickBombCommand::spawn_kick_bomb(
Some(entity),
transform,
element_meta.data.untyped(),
));
}
}
}
Expand All @@ -141,33 +197,26 @@ fn update_idle_kick_bombs(
mut commands: Commands,
mut items_used: CompMut<ItemUsed>,
mut audio_center: ResMut<AudioCenter>,
element_handles: Comp<ElementHandle>,
kick_bomb_handles: Comp<KickBombHandle>,
mut idle_bombs: CompMut<IdleKickBomb>,
assets: Res<AssetServer>,
mut animated_sprites: CompMut<AnimatedSprite>,
) {
for (entity, (_kick_bomb, element_handle)) in
entities.iter_with((&mut idle_bombs, &element_handles))
for (entity, (_kick_bomb, kick_bomb_handle)) in
entities.iter_with((&mut idle_bombs, &kick_bomb_handles))
{
let element_meta = assets.get(element_handle.0);
let kick_bomb_meta = assets.get(kick_bomb_handle.0);

let asset = assets.get(element_meta.data);
let Ok(KickBombMeta {
let KickBombMeta {
fuse_sound,
fuse_sound_volume,
arm_delay,
fuse_time,
..
}) = asset.try_cast_ref()
else {
unreachable!();
};

let arm_delay = *arm_delay;
let fuse_time = *fuse_time;
} = *kick_bomb_meta;

if items_used.remove(entity).is_some() {
audio_center.play_sound(*fuse_sound, *fuse_sound_volume);
audio_center.play_sound(fuse_sound, fuse_sound_volume);
let animated_sprite = animated_sprites.get_mut(entity).unwrap();
animated_sprite.frames = [3, 4, 5].into_iter().collect();
animated_sprite.repeat = true;
Expand All @@ -192,9 +241,8 @@ fn update_idle_kick_bombs(

fn update_lit_kick_bombs(
entities: Res<Entities>,
element_handles: Comp<ElementHandle>,
kick_bomb_handles: Comp<KickBombHandle>,
assets: Res<AssetServer>,

collision_world: CollisionWorld,
player_indexes: Comp<PlayerIdx>,
mut audio_center: ResMut<AudioCenter>,
Expand All @@ -210,12 +258,11 @@ fn update_lit_kick_bombs(
spawners: Comp<DehydrateOutOfBounds>,
invincibles: CompMut<Invincibility>,
) {
for (entity, (kick_bomb, element_handle, spawner)) in
entities.iter_with((&mut lit_grenades, &element_handles, &spawners))
for (entity, (kick_bomb, kick_bomb_handle, spawner)) in
entities.iter_with((&mut lit_grenades, &kick_bomb_handles, &Optional(&spawners)))
{
let element_meta = assets.get(element_handle.0);
let asset = assets.get(element_meta.data);
let Ok(KickBombMeta {
let kick_bomb_meta = assets.get(kick_bomb_handle.0);
let KickBombMeta {
explosion_sound,
explosion_volume,
kick_velocity,
Expand All @@ -226,10 +273,7 @@ fn update_lit_kick_bombs(
explosion_fps,
explosion_frames,
..
}) = asset.try_cast_ref()
else {
unreachable!();
};
} = *kick_bomb_meta;

kick_bomb.fuse_time.tick(time.delta());
kick_bomb.arm_delay.tick(time.delta());
Expand Down Expand Up @@ -271,7 +315,7 @@ fn update_lit_kick_bombs(
let player_standing_left = player_translation.x <= translation.x;

if body.velocity.x == 0.0 {
body.velocity = *kick_velocity;
body.velocity = kick_velocity;
if player_sprite.flip_x {
body.velocity.x *= -1.0;
}
Expand All @@ -293,23 +337,19 @@ fn update_lit_kick_bombs(

// If it's time to explode
if should_explode {
audio_center.play_sound(*explosion_sound, *explosion_volume);
audio_center.play_sound(explosion_sound, explosion_volume);

trauma_events.send(7.5);

// Cause the item to respawn by un-hydrating it's spawner.
hydrated.remove(**spawner);
if let Some(spawner) = spawner {
// Cause the item to respawn by un-hydrating it's spawner.
hydrated.remove(**spawner);
}

let mut explosion_transform = *transforms.get(entity).unwrap();
explosion_transform.translation.z = -10.0; // On top of almost everything
explosion_transform.rotation = Quat::IDENTITY;

// Clone types for move into closure
let damage_region_size = *damage_region_size;
let damage_region_lifetime = *damage_region_lifetime;
let explosion_lifetime = *explosion_lifetime;
let explosion_atlas = *explosion_atlas;
let explosion_fps = *explosion_fps;
let explosion_frames = *explosion_frames;
commands.add(
move |mut entities: ResMutInit<Entities>,
mut transforms: CompMut<Transform>,
Expand Down
2 changes: 2 additions & 0 deletions src/core/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ mod rect;
pub use rect::*;
mod macros;
pub use macros::*;
mod metadata;
pub use metadata::*;
56 changes: 56 additions & 0 deletions src/core/utils/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Utilities for asset and meta data handling.

use crate::prelude::*;

/// Error type for failure to resolve [`UntypedHandle`] to metadata asset from `Handle<T>` or `Handle<ElementMeta>`
/// (where `T` is metadata type).
#[derive(thiserror::Error, Debug, Clone)]
pub enum MetaHandleCastError {
#[error("UntypedHandle does not represent Handle<{0}> or Handle<ElementMeta>")]
BadHandleType(String),
#[error("UntypedHandle maps to Handle<ElementMeta> but ElementMeta inner handle does not cast to {0}")]
ElementDataMismatch(String),
#[error("Failed to retrieve asset for handle (required for cast type valdiation)")]
InvalidHandle,
}

/// Try to get metadata handle from [`UntypedHandle`] that may represent direct handle to meta (`Handle<T>`)
/// or `Handle<ElementMeta>` where [`ElementMeta`]'s data is castable to `Handle<T>`.
///
/// [`Handle::untyped`] can be used to convert to [`UntypedHandle`].
///
/// This is useful for code that wants to spawn an item and take UntypedHandle to allow either direct meta handle
/// or `Handle<ElementMeta>` as argument.
pub fn try_cast_meta_handle<T: HasSchema>(
handle: UntypedHandle,
assets: &AssetServer,
) -> Result<Handle<T>, MetaHandleCastError> {
// Get untyped asset from handle or error on failure
let asset = assets
.try_get_untyped(handle)
.ok_or(MetaHandleCastError::InvalidHandle)?;

// If asset casts to T, return it as Handle<T>
if asset.try_cast_ref::<T>().is_ok() {
return Ok(handle.typed::<T>());
}

// Check if handle type is ElementMeta
if let Ok(element_meta) = asset.try_cast_ref::<ElementMeta>() {
// Does element data cast to T?
if assets.get(element_meta.data).try_cast_ref::<T>().is_ok() {
// Return ElementMeta's data as Handle<T>
Ok(element_meta.data.untyped().typed())
} else {
// ElementMeta data does not cast to T.
Err(MetaHandleCastError::ElementDataMismatch(
std::any::type_name::<T>().to_string(),
))
}
} else {
// UntypedHandle is neither Handle<T> or Handle<ElementMeta>.
Err(MetaHandleCastError::BadHandleType(
std::any::type_name::<T>().to_string(),
))
}
}
Loading