diff --git a/Cargo.lock b/Cargo.lock index 966fca9a335ba5342913ea526686a48efcd86c74..02d372ed069c5b2183897bd8a88602ed8f7d72e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -734,6 +734,16 @@ dependencies = [ "bevy_reflect", ] +[[package]] +name = "bevy_tweening" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "164bcb41708fa1aeb435b6bba6cae61777b9f8cf3e18b207bb6209ca5b970e77" +dependencies = [ + "bevy", + "interpolation", +] + [[package]] name = "bevy_ui" version = "0.8.1" @@ -1491,6 +1501,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bevy", + "bevy_tweening", "fastrand", "iyes_loopless", "log 0.4.17", @@ -1799,6 +1810,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "interpolation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7357d2bbc5ee92f8e899ab645233e43d21407573cceb37fed8bc3dede2c02" + [[package]] name = "iovec" version = "0.1.4" diff --git a/game_core/Cargo.toml b/game_core/Cargo.toml index 268949dafb357ee4661e16be8a906cfcf63bcd92..56d0c1efdeda1551239740b0c191bc7ba377c7b1 100644 --- a/game_core/Cargo.toml +++ b/game_core/Cargo.toml @@ -21,6 +21,7 @@ iyes_loopless = "0.7.1" micro_musicbox = { version = "0.4.0", features = ["serde", "mp3"] } remote_events = { git = "https://lab.lcr.gr/microhacks/micro-bevy-remote-events.git", rev = "be0c6b43a73e4c5e7ece20797e3d6f59340147b4"} +bevy_tweening = "0.5.0" [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3.60", features = ["Window"] } diff --git a/game_core/index.html b/game_core/index.html index ea8e997b61896868564fe7747edd45ef9352b0d6..925583234e93b163b8357e303b8e27754368b77d 100644 --- a/game_core/index.html +++ b/game_core/index.html @@ -46,8 +46,9 @@ } #start_button { - width: 25%; + width: auto; height: 25%; + aspect-ratio: 1; background-color: black; border-radius: 50%; cursor: pointer; diff --git a/game_core/src/control/mod.rs b/game_core/src/control/mod.rs index d2f14fac6d4b4a4e5d93bb5e6677b4feb9847136..1f1c2de93e06ffb74e937b718c9b40234433fab8 100644 --- a/game_core/src/control/mod.rs +++ b/game_core/src/control/mod.rs @@ -13,7 +13,7 @@ mod __plugin { impl Plugin for ControlPlugin { fn build(&self, app: &mut App) { app.add_system_set_to_stage( - CoreStage::PreUpdate, + CoreStage::First, ConditionSet::new() .run_in_state(AppState::InGame) .with_system(super::player::handle_player_input) diff --git a/game_core/src/control/player.rs b/game_core/src/control/player.rs index 63f571f8a9361ccbac9d283e5da79133441dba68..aadd6053e53a01d66423ccb39df381c4ddd17eaf 100644 --- a/game_core/src/control/player.rs +++ b/game_core/src/control/player.rs @@ -4,9 +4,10 @@ use bevy::math::ivec2; use bevy::prelude::*; use crate::control::ai::{Automated, ShouldAct}; +use crate::entities::animations::{PositionBundle, PositionTween}; use crate::entities::lifecycle::Player; use crate::entities::timing::ActionCooldown; -use crate::world::level_map::GridPosition; +use crate::world::level_map::{GridPosition, WORLD_TILE_SIZE}; pub fn handle_player_input( mut commands: Commands, @@ -36,10 +37,17 @@ pub fn handle_player_input( if dx != 0 || dy != 0 { for (entity, mut position) in &mut player_query { if dx != 0 || dy != 0 { - let next_position = (position.0.as_ivec2()) + ivec2(dx, dy); - **position = next_position.as_uvec2(); + let current_position = **position; + let next_position = ((current_position.as_ivec2()) + ivec2(dx, dy)).as_uvec2(); + + **position = next_position; commands .entity(entity) + // .insert_bundle(PositionBundle::from(PositionTween::new( + // next_position.as_vec2() * WORLD_TILE_SIZE + WORLD_TILE_SIZE / 2.0, + // current_position.as_vec2() * WORLD_TILE_SIZE + WORLD_TILE_SIZE / 2.0, + // Duration::from_millis(100), + // ))) .insert(ActionCooldown::from(Duration::from_millis(250))) .remove::<ShouldAct>(); } diff --git a/game_core/src/entities/animations.rs b/game_core/src/entities/animations.rs new file mode 100644 index 0000000000000000000000000000000000000000..272f6d342b542215a35952b3eaaa9de3e4f17b33 --- /dev/null +++ b/game_core/src/entities/animations.rs @@ -0,0 +1,157 @@ +use std::ops::{Deref, DerefMut}; +use std::time::Duration; + +use bevy::math::vec2; +use bevy::prelude::*; +use iyes_loopless::prelude::ConditionSet; +use log::LevelFilter::Off; + +use crate::system::flow::AppState; + +#[derive(Clone, Debug, Component)] +pub struct FrameAnimation { + frames: Vec<usize>, + current_index: usize, + frame_time: Duration, + current_time: Duration, +} + +impl FrameAnimation { + pub fn new(frames: Vec<usize>, frame_time: Duration) -> Self { + Self { + current_index: 0, + frames, + frame_time, + current_time: Duration::ZERO, + } + } +} + +pub fn tick_frame_animations( + time: Res<Time>, + mut query: Query<(&mut FrameAnimation, &mut TextureAtlasSprite)>, +) { + let dt = time.delta(); + for (mut animation, mut sprite) in &mut query { + animation.current_time += dt; + while animation.current_time > animation.frame_time { + animation.current_index += 1; + if animation.current_index == animation.frames.len() { + animation.current_index = 0; + } + let new_time = animation.current_time - animation.frame_time; + animation.current_time = new_time; + } + sprite.index = animation.frames[animation.current_index]; + } +} + +#[derive(Clone, Debug, Component)] +pub struct Offset(Vec2); +impl From<Vec2> for Offset { + fn from(other: Vec2) -> Self { + Self(other) + } +} +impl Deref for Offset { + type Target = Vec2; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for Offset { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Clone, Debug, Component)] +pub struct PositionTween { + from: Vec2, + to: Vec2, + duration: Duration, + elapsed: Duration, +} + +impl PositionTween { + pub fn new(from: Vec2, to: Vec2, duration: Duration) -> Self { + Self { + from, + to, + duration, + elapsed: Duration::ZERO, + } + } + + pub fn get_value(&self) -> Vec2 { + if self.elapsed == Duration::ZERO { + self.to - self.from + } else { + (self.to - self.from) * (self.elapsed.as_secs_f32() / self.duration.as_secs_f32()) + } + } +} + +#[derive(Bundle)] +pub struct PositionBundle { + position: PositionTween, + offset: Offset, +} + +impl From<PositionTween> for PositionBundle { + fn from(other: PositionTween) -> Self { + PositionBundle { + offset: other.get_value().into(), + position: other, + } + } +} + +pub fn sync_tween_offset(time: Res<Time>, mut query: Query<(&mut PositionTween, &mut Offset)>) { + let delta = time.delta(); + + for (mut tween, mut offset) in &mut query { + tween.elapsed = tween.duration.min(tween.elapsed + delta); + let percent = tween.elapsed.as_secs_f32() / tween.duration.as_secs_f32(); + **offset = (tween.to - tween.from) * percent; + } +} + +pub fn tween_cleanup(mut commands: Commands, query: Query<(Entity, &PositionTween)>) { + for (entity, position) in &query { + if position.elapsed >= position.duration { + commands + .entity(entity) + .remove::<PositionTween>() + .remove::<Offset>(); + } + } +} + +pub struct AnimationsPlugin; +impl Plugin for AnimationsPlugin { + fn build(&self, app: &mut App) { + // app.add_system_set_to_stage( + // CoreStage::PreUpdate, + // ConditionSet::new() + // .run_in_state(AppState::InGame) + // .with_system(add_offset_to_tweened) + // .into(), + // ) + app.add_system_set_to_stage( + CoreStage::Update, + ConditionSet::new() + .run_in_state(AppState::InGame) + .with_system(sync_tween_offset) + .with_system(tick_frame_animations) + .into(), + ) + .add_system_set_to_stage( + CoreStage::Last, + ConditionSet::new() + .run_in_state(AppState::InGame) + .with_system(tween_cleanup) + .into(), + ); + } +} diff --git a/game_core/src/entities/curses.rs b/game_core/src/entities/curses.rs new file mode 100644 index 0000000000000000000000000000000000000000..99d63907a7efbc650ecb8490e9d8bd074f224d22 --- /dev/null +++ b/game_core/src/entities/curses.rs @@ -0,0 +1,87 @@ +use std::fmt::{Display, Formatter}; + +use bevy::prelude::*; +use iyes_loopless::prelude::ConditionSet; + +use crate::entities::spawner::EntitySpawner; +use crate::entities::timing::GlobalTimer; +use crate::system::flow::AppState; + +#[derive(Clone, Copy, Debug, Component)] +pub struct CurseText; + +pub fn generate_curse(timer: Res<GlobalTimer>, mut events: EventWriter<CurseEvent>) { + if timer.is_triggered() { + let curse = match fastrand::usize(0..60) { + 0..=9 => CurseEvent::CurseOfAmnesia, + 10..=19 => CurseEvent::CurseOfPain, + 20..=29 => CurseEvent::CurseOfMonsters, + 30..=39 => CurseEvent::CurseOfWeakness, + 40..=49 => CurseEvent::CurseOfLoss, + 50..=59 => CurseEvent::CurseOfMidas, + _ => CurseEvent::CurseOfPain, + }; + + events.send(curse); + } +} + +pub enum CurseEvent { + CurseOfPain, + CurseOfWeakness, + CurseOfMonsters, + CurseOfAmnesia, + CurseOfMidas, + CurseOfLoss, +} + +impl Display for CurseEvent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + CurseEvent::CurseOfPain => "Curse of Pain", + CurseEvent::CurseOfWeakness => "Curse of Weakness", + CurseEvent::CurseOfMonsters => "Curse of Monsters", + CurseEvent::CurseOfAmnesia => "Curse of Amnesia", + CurseEvent::CurseOfMidas => "Curse of Midas", + CurseEvent::CurseOfLoss => "Curse of Loss", + } + ) + } +} + +pub fn handle_curse( + mut spawner: EntitySpawner, + mut events: ResMut<Events<CurseEvent>>, + query: Query<Entity, With<CurseText>>, +) { + for event in events.drain() { + for entity in &query { + spawner.commands.entity(entity).despawn_recursive(); + } + spawner.spawn_curse_text(event); + } +} + +pub struct CursesPlugin; +impl Plugin for CursesPlugin { + fn build(&self, app: &mut App) { + app.add_event::<CurseEvent>() + .add_system_set_to_stage( + CoreStage::PreUpdate, + ConditionSet::new() + .run_in_state(AppState::InGame) + .with_system(generate_curse) + .into(), + ) + .add_system_set_to_stage( + CoreStage::PostUpdate, + ConditionSet::new() + .run_in_state(AppState::InGame) + .with_system(handle_curse) + .into(), + ); + } +} diff --git a/game_core/src/entities/mod.rs b/game_core/src/entities/mod.rs index 89dfaa7f89d9e5af74214bd9c2585387419ea181..0568d6a81b6039b620789ace50ba0b1342d280b2 100644 --- a/game_core/src/entities/mod.rs +++ b/game_core/src/entities/mod.rs @@ -1,3 +1,5 @@ +pub mod animations; +pub mod curses; pub mod lifecycle; pub mod spawner; pub mod timing; @@ -8,7 +10,10 @@ mod __plugin { pub struct EntitiesPluginGroup; impl PluginGroup for EntitiesPluginGroup { fn build(&mut self, group: &mut PluginGroupBuilder) { - group.add(super::timing::TimingPlugin); + group + .add(super::timing::TimingPlugin) + .add(super::animations::AnimationsPlugin) + .add(super::curses::CursesPlugin); } } } diff --git a/game_core/src/entities/spawner.rs b/game_core/src/entities/spawner.rs index 7547a7451ce9e78899a8b07a0d298713ddb034f8..545508fa0bb8fe53280bdd84b5b65ebc78c9e7e7 100644 --- a/game_core/src/entities/spawner.rs +++ b/game_core/src/entities/spawner.rs @@ -1,8 +1,14 @@ +use std::time::Duration; + use bevy::ecs::system::SystemParam; use bevy::prelude::*; +use bevy_tweening::lens::{TextColorLens, UiPositionLens}; +use bevy_tweening::{Animator, EaseFunction, Tween, TweeningType}; use crate::assets::AssetHandles; use crate::control::ai::ShouldAct; +use crate::entities::animations::FrameAnimation; +use crate::entities::curses::{CurseEvent, CurseText}; use crate::entities::lifecycle::{GameEntity, Player}; use crate::system::camera::ChaseCam; use crate::system::graphics::LAYER_CREATURE; @@ -25,6 +31,10 @@ impl<'w, 's> EntitySpawner<'w, 's> { .insert(GameEntity) .insert(Player) .insert(ShouldAct) + .insert(FrameAnimation::new( + vec![PLAYER_SPRITE, PLAYER_SPRITE + 1], + Duration::from_millis(250), + )) .insert(GridPosition(grid_position)); entity.insert_bundle(SpriteSheetBundle { @@ -37,4 +47,55 @@ impl<'w, 's> EntitySpawner<'w, 's> { ..Default::default() }); } + + pub fn spawn_curse_text(&mut self, curse: CurseEvent) { + let position_animation = Tween::new( + EaseFunction::ExponentialIn, + TweeningType::Once, + Duration::from_millis(750), + UiPositionLens { + start: UiRect::new(Val::Auto, Val::Px(20.0), Val::Px(200.0), Val::Auto), + end: UiRect::new(Val::Auto, Val::Px(20.0), Val::Px(20.0), Val::Auto), + }, + ); + + let colour_in_animation = Tween::new( + EaseFunction::SineIn, + TweeningType::Once, + Duration::from_millis(500), + TextColorLens { + section: 0, + start: (*Color::GOLD.set_a(0.0)).into(), + end: (*Color::GOLD.set_a(1.0)).into(), + }, + ); + + self.commands + .spawn_bundle(NodeBundle { + color: Color::rgba(0.0, 0.0, 0.0, 0.0).into(), + style: Style { + position: UiRect::new(Val::Auto, Val::Px(20.0), Val::Px(100.0), Val::Auto), + position_type: PositionType::Absolute, + ..Default::default() + }, + ..Default::default() + }) + .insert(Animator::new(position_animation)) + .insert(CurseText) + .with_children(|builder| { + builder + .spawn_bundle(TextBundle { + text: Text::from_section( + curse.to_string(), + TextStyle { + color: (*Color::GOLD.set_a(0.0)), + font: self.handles.font("main"), + font_size: 32.0, + }, + ), + ..Default::default() + }) + .insert(Animator::new(colour_in_animation)); + }); + } } diff --git a/game_core/src/entities/timing.rs b/game_core/src/entities/timing.rs index e210d3c72e7416bf8a0598030d3aaae4f140bd56..62d3232f8e0f42cfa82223d12025577eeeffdeb3 100644 --- a/game_core/src/entities/timing.rs +++ b/game_core/src/entities/timing.rs @@ -21,6 +21,9 @@ impl Default for GlobalTimer { impl GlobalTimer { const GOAL: Duration = Duration::from_secs(10); + pub fn new() -> Self { + Self::default() + } pub fn tick(&mut self, dt: Duration) { self.internal += dt; } @@ -32,6 +35,9 @@ impl GlobalTimer { self.internal -= Self::GOAL } } + pub fn reset(&mut self) { + self.internal = Duration::ZERO; + } pub fn percent_complete(&self) -> f32 { self.internal.as_secs_f32() / Self::GOAL.as_secs_f32() } @@ -42,6 +48,10 @@ pub fn tick_global_timer(time: Res<Time>, mut timer: ResMut<GlobalTimer>) { timer.tick(time.delta()); } +pub fn reset_timer(mut timer: ResMut<GlobalTimer>) { + timer.reset(); +} + #[derive(Copy, Clone, Default, Component)] pub struct GlobalTimerUi; pub fn spawn_global_timer_ui(mut commands: Commands, timer: Res<GlobalTimer>) { @@ -58,7 +68,7 @@ pub fn spawn_global_timer_ui(mut commands: Commands, timer: Res<GlobalTimer>) { ), ..Default::default() }, - color: Color::ALICE_BLUE.into(), + color: Color::ORANGE_RED.into(), ..Default::default() }) .insert(GlobalTimerUi); @@ -119,6 +129,7 @@ impl Plugin for TimingPlugin { fn build(&self, app: &mut App) { app.init_resource::<GlobalTimer>() .add_system_to_stage(CoreStage::First, tick_global_timer) + .add_enter_system(AppState::InGame, reset_timer) .add_enter_system(AppState::InGame, spawn_global_timer_ui) .add_system_set( ConditionSet::new() diff --git a/game_core/src/main.rs b/game_core/src/main.rs index 4823b6c706c61a0263cd61189cb5973d49c3997a..66063751d05c52e867e27462aac73d383f0c41e3 100644 --- a/game_core/src/main.rs +++ b/game_core/src/main.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use bevy_tweening::TweeningPlugin; use game_core::assets::AssetHandles; use game_core::system::flow::AppState; use game_core::system::resources::DefaultResourcesPlugin; @@ -23,5 +24,6 @@ fn main() { .add_plugin(game_core::world::WorldPlugin) .add_plugin(game_core::control::ControlPlugin) .add_plugins(game_core::entities::EntitiesPluginGroup) + .add_plugin(TweeningPlugin) .run(); } diff --git a/game_core/src/system/load_config.rs b/game_core/src/system/load_config.rs index 5481bd2250cf6d9622f6e0bc0cb558ba311c11a0..a2d19c1784f392676142ea3fd1a1f5c058cf4f11 100644 --- a/game_core/src/system/load_config.rs +++ b/game_core/src/system/load_config.rs @@ -15,7 +15,7 @@ mod setup { } pub fn viewport_size() -> (f32, f32) { - (16.0 * 32.0, 16.0 * 18.0) + (16.0 * 24.0, 16.0 * 13.5) } } diff --git a/game_core/src/world/generators/mod.rs b/game_core/src/world/generators/mod.rs index c8dc5112ebbcbd04b5e784d63abb4dc0af4bed2e..4a4fa09d53dfa58d4e1b8e763ac6bfc921b09f28 100644 --- a/game_core/src/world/generators/mod.rs +++ b/game_core/src/world/generators/mod.rs @@ -6,8 +6,8 @@ pub mod blobular; pub mod drunkard_corridor; pub(crate) mod utils; -pub(crate) const TMP_FLOOR_GROUP: usize = 6 * 64; -pub(crate) const TMP_WALL_GROUP: usize = 3 * 64 + 28; +pub(crate) const TMP_FLOOR_GROUP: usize = 18 * 64; +pub(crate) const TMP_WALL_GROUP: usize = 18 * 64 + 28; pub trait MapGenerator { fn generate(indexer: &Indexer, rng: &Rng) -> LevelMap; diff --git a/game_core/src/world/level_map.rs b/game_core/src/world/level_map.rs index 633689c905a3a3b670fab28bac38e7cbafbbf216..e327719c909e923362baf31e6eab01af2fab5656 100644 --- a/game_core/src/world/level_map.rs +++ b/game_core/src/world/level_map.rs @@ -5,6 +5,7 @@ use bevy::math::UVec2; use bevy::prelude::*; use fastrand::Rng; +use crate::entities::animations::Offset; use crate::world::adjacency::{ get_floor_sprite_offset, get_wall_sprite_offset, BOTTOM, LEFT, NONE, RIGHT, TOP, }; @@ -34,10 +35,12 @@ impl DerefMut for GridPosition { /// Take an entity's position on a grid, and sync it up to the transform used to render /// the sprite -pub fn sync_grid_to_transform(mut query: Query<(&GridPosition, &mut Transform)>) { - for (position, mut transform) in &mut query { - transform.translation = ((position.as_vec2() * WORLD_TILE_SIZE) + (WORLD_TILE_SIZE / 2.0)) - .extend(transform.translation.z); +pub fn sync_grid_to_transform(mut query: Query<(&GridPosition, &mut Transform, Option<&Offset>)>) { + for (position, mut transform, offset) in &mut query { + transform.translation = ((position.as_vec2() * WORLD_TILE_SIZE) + + (WORLD_TILE_SIZE / 2.0) + + offset.map(|offset| **offset).unwrap_or(Vec2::ZERO)) + .extend(transform.translation.z); } }