Skip to content

Commit

Permalink
fix(kick_bomb): Add spawn kickbomb command to allow spawning from gam…
Browse files Browse the repository at this point in the history
…eplay code without interfering with map hydration (fishfolk#968)

`commands.add(KickBombCommand::spawn_kick_bomb(Some(entity), transform,
kick_bomb_meta_handle)` may be used to spawn a kick bomb.
- If entity is passed in it will initialize kick bomb components on this
entity (mostly to support hydration from map elements), otherwise None
will make it create an entity for you.
- The handle is untyped handle (can use `handle.untyped()` to convert to
this), it supports `Handle<ElementMeta>` or `Handle<KickBombMeta>`
automatically.

This command adds a `KickBombHandle` containing `Handle<KickBombMeta>`,
this should be used to get meta for entities. The kick bomb systems now
use this instead of `Handle<ElementMeta>`.

Gameplay systems were also updated such that usages of components
`MapElementHydrated` and `DehydratedOutOfBounds` (things specific to map
spawned kickbombs) are optionally used, meaning these systems still run
for gameplay spawned kickbombs missing these components.

Under the hood, `try_cast_meta_handle` helper function was added to help
convert an untyped `Handle<KickBombMeta>` or `Handle<ElementMeta>` to
`Handle<KickBombMeta>`. This function is generic and may be used for
other gameplay items that we refactor to add spawn commands to separate
item spawn logic from map spawning.

I tested this and the map spawned kickbombs will still respawn after
use, but the gameplay spawned ones do not.
  • Loading branch information
MaxCWhitehead authored and DRuppFv committed Apr 20, 2024
1 parent 8f3cd11 commit 609b11c
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 92 deletions.
224 changes: 132 additions & 92 deletions src/core/elements/kick_bomb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,86 +55,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 @@ -144,35 +200,28 @@ 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,
lit_frames,
lit_fps,
..
}) = 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 = lit_frames.clone();
animated_sprite.repeat = true;
Expand All @@ -197,9 +246,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 @@ -215,12 +263,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 @@ -232,10 +279,7 @@ fn update_lit_kick_bombs(
explosion_frames,
explode_on_contact,
..
}) = 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 @@ -295,7 +339,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 @@ -317,23 +361,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(),
))
}
}

0 comments on commit 609b11c

Please sign in to comment.