diff --git a/src/components/mod.rs b/src/components/mod.rs index 00c3c68..556bd61 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -3,4 +3,5 @@ pub mod draw_common; pub mod flash; pub mod hud; pub mod inventory; +pub mod number_popup; pub mod stage_select; diff --git a/src/components/number_popup.rs b/src/components/number_popup.rs new file mode 100644 index 0000000..7168f70 --- /dev/null +++ b/src/components/number_popup.rs @@ -0,0 +1,100 @@ +use crate::common::{fix9_scale, interpolate_fix9_scale, Rect}; +use crate::entity::GameEntity; +use crate::frame::Frame; +use crate::framework::context::Context; +use crate::framework::error::GameResult; +use crate::shared_game_state::SharedGameState; + +#[derive(Debug, Copy, Clone)] +pub struct NumberPopup { + pub value: i16, + pub x: i32, + pub y: i32, + pub prev_x: i32, + pub prev_y: i32, + counter: u16, +} + +impl NumberPopup { + pub fn new() -> NumberPopup { + NumberPopup { value: 0, x: 0, y: 0, prev_x: 0, prev_y: 0, counter: 0 } + } + + pub fn set_value(&mut self, value: i16) { + if self.counter > 32 { + self.counter = 32; + } + + self.value = value; + } + + pub fn add_value(&mut self, value: i16) { + self.set_value(self.value + value); + } +} + +impl GameEntity<()> for NumberPopup { + fn tick(&mut self, _state: &mut SharedGameState, _custom: ()) -> GameResult<()> { + if self.value == 0 { + return Ok(()); + } + + self.counter += 1; + if self.counter == 80 { + self.counter = 0; + self.value = 0; + } + + Ok(()) + } + + fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, frame: &Frame) -> GameResult<()> { + if self.value == 0 { + return Ok(()); + } + + // tick 0 - 32 - move up by 0.5 pixels + // tick 33 - 72 - stay + // tick 73 - 80 - fade up + let y_offset = self.counter.min(32) as f32 * 0.5; + let clip = self.counter.max(72) - 72; + + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?; + + let (frame_x, frame_y) = frame.xy_interpolated(state.frame_time); + let x = interpolate_fix9_scale(self.prev_x, self.x, state.frame_time) - frame_x; + let y = interpolate_fix9_scale(self.prev_x, self.y, state.frame_time) - frame_y - y_offset; + + let mut n = self.value.to_string(); + if self.value > 0 { + n = "+".to_owned() + n.as_str(); + }; + + let x = x - n.len() as f32 * 4.0; + + for (offset, chr) in n.chars().enumerate() { + match chr { + '+' => { + batch.add_rect(x + offset as f32 * 8.0, y, &Rect::new_size(32, 48 + clip, 8, 8 - clip)); + } + '-' => { + batch.add_rect(x + offset as f32 * 8.0, y, &Rect::new_size(40, 48 + clip, 8, 8 - clip)); + } + '0'..='9' => { + let number_set = if self.value < 0 { 64 } else { 56 }; + let idx = chr as u16 - '0' as u16; + batch.add_rect( + x + offset as f32 * 8.0, + y, + &Rect::new_size(idx * 8, number_set + clip, 8, 8 - clip), + ); + } + _ => {} + } + } + + batch.draw(ctx)?; + + Ok(()) + } +} diff --git a/src/npc/mod.rs b/src/npc/mod.rs index a652c12..4c18005 100644 --- a/src/npc/mod.rs +++ b/src/npc/mod.rs @@ -21,6 +21,7 @@ use crate::shared_game_state::SharedGameState; use crate::stage::Stage; use crate::str; use crate::components::flash::Flash; +use crate::components::number_popup::NumberPopup; pub mod ai; pub mod boss; @@ -107,6 +108,7 @@ pub struct NPC { pub display_bounds: Rect, pub hit_bounds: Rect, pub rng: Xoroshiro32PlusPlus, + pub popup: NumberPopup, } impl NPC { @@ -149,6 +151,7 @@ impl NPC { anim_counter: 0, anim_rect: Rect { left: 0, top: 0, right: 0, bottom: 0 }, rng: Xoroshiro32PlusPlus::new(0), + popup: NumberPopup::new(), } } @@ -380,6 +383,10 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &BulletManager, &mut Fl }, }?; + self.popup.x = self.x; + self.popup.y = self.y; + self.popup.tick(state, ())?; + if self.shock > 0 { self.shock -= 1; } @@ -421,6 +428,8 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &BulletManager, &mut Fl ); batch.draw(ctx)?; + self.popup.draw(state, ctx, frame)?; + Ok(()) } } diff --git a/src/npc/utils.rs b/src/npc/utils.rs index af15b4f..2b67ed9 100644 --- a/src/npc/utils.rs +++ b/src/npc/utils.rs @@ -11,6 +11,7 @@ use crate::npc::list::NPCList; use crate::player::Player; use crate::rng::{RNG, Xoroshiro32PlusPlus}; use crate::shared_game_state::SharedGameState; +use crate::components::number_popup::NumberPopup; impl NPC { /// Initializes the RNG. Called when the [NPC] is being added to an [NPCList]. @@ -80,6 +81,7 @@ impl NPC { anim_counter: 0, anim_rect: Rect::new(0, 0, 0, 0), rng: Xoroshiro32PlusPlus::new(0), + popup: NumberPopup::new(), } } diff --git a/src/player/mod.rs b/src/player/mod.rs index 47f3adc..5e599b7 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -17,6 +17,7 @@ use crate::player::skin::{PlayerAnimationState, PlayerAppearanceState, PlayerSki use crate::player::skin::basic::BasicPlayerSkin; use crate::rng::RNG; use crate::shared_game_state::SharedGameState; +use crate::components::number_popup::NumberPopup; mod player_hit; pub mod skin; @@ -72,6 +73,7 @@ pub struct Player { pub air: u16, pub skin: Box, pub controller: Box, + pub popup: NumberPopup, weapon_offset_y: i8, camera_target_x: i32, camera_target_y: i32, @@ -126,6 +128,7 @@ impl Player { air: 0, skin: Box::new(BasicPlayerSkin::new("MyChar".to_string())), controller: Box::new(DummyPlayerController::new()), + popup: NumberPopup::new(), damage_counter: 0, damage_taken: 0, anim_num: 0, @@ -685,6 +688,11 @@ impl Player { } self.damage = self.damage.saturating_add(hp as u16); + if self.popup.value > 0 { + self.popup.set_value(-(self.damage as i16)); + } else { + self.popup.add_value(-(self.damage as i16)); + } if self.life == 0 { state.sound_manager.play_sfx(17); @@ -729,6 +737,10 @@ impl GameEntity<&NPCList> for Player { ControlMode::IronHead => self.tick_ironhead(state)?, } + self.popup.x = self.x; + self.popup.y = self.y; + self.popup.tick(state, ())?; + self.cond.set_increase_acceleration(false); self.tick_animation(state); diff --git a/src/player/player_hit.rs b/src/player/player_hit.rs index c6667db..d491a80 100644 --- a/src/player/player_hit.rs +++ b/src/player/player_hit.rs @@ -258,6 +258,13 @@ impl Player { 1 => { state.sound_manager.play_sfx(14); inventory.add_xp(npc.exp, self, state); + + if self.popup.value > 0 { + self.popup.add_value(npc.exp as i16); + } else { + self.popup.set_value(npc.exp as i16); + } + npc.cond.set_alive(false); } // missile pickup diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index e2c6037..8054b8d 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -1161,7 +1161,7 @@ impl GameScene { if npc.life == 0 { if npc.npc_flags.show_damage() { - // todo show damage + npc.popup.add_value(-bullet.damage); } if self.player1.cond.alive() && npc.npc_flags.event_when_killed() { @@ -1190,7 +1190,7 @@ impl GameScene { } if npc.npc_flags.show_damage() { - // todo show damage + npc.popup.add_value(-bullet.damage); } } } else if !bullet.weapon_flags.flag_x10() @@ -1704,18 +1704,26 @@ impl Scene for GameScene { self.frame.prev_y = self.frame.y; self.player1.prev_x = self.player1.x; self.player1.prev_y = self.player1.y; + self.player1.popup.prev_x = self.player1.prev_x; + self.player1.popup.prev_y = self.player1.prev_y; self.player2.prev_x = self.player2.x; self.player2.prev_y = self.player2.y; + self.player2.popup.prev_x = self.player2.prev_x; + self.player2.popup.prev_y = self.player2.prev_y; for npc in self.npc_list.iter_alive() { npc.prev_x = npc.x; npc.prev_y = npc.y; + npc.popup.prev_x = npc.prev_x; + npc.popup.prev_y = npc.prev_y; } for npc in self.boss.parts.iter_mut() { if npc.cond.alive() { npc.prev_x = npc.x; npc.prev_y = npc.y; + npc.popup.prev_x = npc.prev_x; + npc.popup.prev_y = npc.prev_y; } } @@ -1762,6 +1770,9 @@ impl Scene for GameScene { self.draw_tiles(state, ctx, TileLayer::Foreground)?; self.draw_tiles(state, ctx, TileLayer::Snack)?; self.draw_carets(state, ctx)?; + self.player1.popup.draw(state, ctx, &self.frame)?; + self.player2.popup.draw(state, ctx, &self.frame)?; + if state.settings.shader_effects && (self.stage.data.background_type == BackgroundType::Black || self.stage.data.background.name() == "bkBlack")