use agb::display::object::{OamManaged, Object}; use agb::fixnum::Vector2D; use agb::rng; use alloc::vec; use alloc::vec::Vec; use crate::graphics::{BURST_BULLET, DISRUPT_BULLET, SHIELD}; use crate::sfx::Sfx; use crate::{ graphics::{ FractionDisplay, HealthBar, NumberDisplay, BULLET_SPRITE, ENEMY_ATTACK_SPRITES, FACE_SPRITES, SHIP_SPRITES, }, EnemyAttackType, Ship, }; use super::{Action, CurrentBattleState, EnemyAttackState, MALFUNCTION_COOLDOWN_FRAMES}; struct BattleScreenDisplayObjects<'a> { dice: Vec>, dice_cooldowns: Vec>, player_shield: Vec>, enemy_shield: Vec>, player_healthbar: HealthBar<'a>, enemy_healthbar: HealthBar<'a>, player_health: FractionDisplay<'a>, enemy_health: FractionDisplay<'a>, enemy_attack_display: Vec>, } pub struct BattleScreenDisplay<'a> { objs: BattleScreenDisplayObjects<'a>, animations: Vec>, _misc_sprites: Vec>, } const HEALTH_BAR_WIDTH: usize = 48; impl<'a> BattleScreenDisplay<'a> { pub fn new(obj: &'a OamManaged, current_battle_state: &CurrentBattleState) -> Self { let mut misc_sprites = vec![]; let player_x = 12; let player_y = 8; let enemy_x = 167; let player_sprite = SHIP_SPRITES.sprite_for_ship(Ship::Player); let enemy_sprite = SHIP_SPRITES.sprite_for_ship(if rng::gen() % 2 == 0 { Ship::Drone } else { Ship::PilotedShip }); let mut player_obj = obj.object_sprite(player_sprite); let mut enemy_obj = obj.object_sprite(enemy_sprite); player_obj.set_x(player_x).set_y(player_y).set_z(1).show(); enemy_obj.set_x(enemy_x).set_y(player_y).set_z(1).show(); misc_sprites.push(player_obj); misc_sprites.push(enemy_obj); let dice: Vec<_> = current_battle_state .rolled_dice .faces_to_render() .enumerate() .map(|(i, (face, _))| { let mut die_obj = obj.object_sprite(FACE_SPRITES.sprite_for_face(face)); die_obj.set_y(120).set_x(i as u16 * 40 + 28).show(); die_obj }) .collect(); let dice_cooldowns: Vec<_> = dice .iter() .enumerate() .map(|(i, _)| { let mut cooldown_bar = HealthBar::new((i as i32 * 40 + 28, 120 - 8).into(), 24, obj); cooldown_bar.hide(); cooldown_bar }) .collect(); let shield_sprite = SHIP_SPRITES.sprite_for_ship(Ship::Shield); let player_shield: Vec<_> = (0..5) .map(|i| { let mut shield_obj = obj.object_sprite(shield_sprite); shield_obj .set_x(player_x + 18 + 11 * i) .set_y(player_y) .hide(); shield_obj }) .collect(); let enemy_shield: Vec<_> = (0..5) .map(|i| { let mut shield_obj = obj.object_sprite(shield_sprite); shield_obj .set_x(enemy_x - 16 - 11 * i) .set_y(player_y) .set_hflip(true) .hide(); shield_obj }) .collect(); let player_healthbar_x = 18; let enemy_healthbar_x = 180; let player_healthbar = HealthBar::new( (player_healthbar_x, player_y - 8).into(), HEALTH_BAR_WIDTH, obj, ); let enemy_healthbar = HealthBar::new( (enemy_healthbar_x, player_y - 8).into(), HEALTH_BAR_WIDTH, obj, ); let player_health_display = FractionDisplay::new( ( player_healthbar_x + HEALTH_BAR_WIDTH as u16 / 2 - 16, player_y, ) .into(), 3, obj, ); let enemy_health_display = FractionDisplay::new( ( enemy_healthbar_x + HEALTH_BAR_WIDTH as u16 / 2 - 16, player_y, ) .into(), 3, obj, ); let enemy_attack_display = (0..2) .map(|i| { let mut attack_obj = obj .object_sprite(ENEMY_ATTACK_SPRITES.sprite_for_attack(EnemyAttackType::Attack)); let attack_obj_position = Vector2D::new(120, 56 + 32 * i); attack_obj.set_position(attack_obj_position).hide(); let mut attack_cooldown = HealthBar::new(attack_obj_position + (32, 8).into(), 48, obj); attack_cooldown.hide(); let attack_number_display = NumberDisplay::new(attack_obj_position - (8, -10).into()); EnemyAttackDisplay::new(attack_obj, attack_cooldown, attack_number_display) }) .collect(); let objs = BattleScreenDisplayObjects { dice, dice_cooldowns, player_shield, enemy_shield, player_healthbar, enemy_healthbar, player_health: player_health_display, enemy_health: enemy_health_display, enemy_attack_display, }; Self { objs, animations: vec![], _misc_sprites: misc_sprites, } } pub fn update( &mut self, obj: &'a OamManaged, current_battle_state: &CurrentBattleState, ) -> Vec { for (i, player_shield) in self.objs.player_shield.iter_mut().enumerate() { if i < current_battle_state.player.shield_count as usize { player_shield .show() .set_sprite(obj.sprite(SHIELD.sprite(0))); } else { player_shield.hide(); } } for (i, player_shield) in self.objs.enemy_shield.iter_mut().enumerate() { if i < current_battle_state.enemy.shield_count as usize { player_shield .show() .set_sprite(obj.sprite(SHIELD.sprite(0))); } else { player_shield.hide(); } } self.objs.player_healthbar.set_value( ((current_battle_state.player.health * HEALTH_BAR_WIDTH as u32) / current_battle_state.player.max_health) as usize, obj, ); self.objs.enemy_healthbar.set_value( ((current_battle_state.enemy.health * HEALTH_BAR_WIDTH as u32) / current_battle_state.enemy.max_health) as usize, obj, ); self.objs.player_health.set_value( current_battle_state.player.health as usize, current_battle_state.player.max_health as usize, obj, ); self.objs.enemy_health.set_value( current_battle_state.enemy.health as usize, current_battle_state.enemy.max_health as usize, obj, ); for (i, attack) in current_battle_state.attacks.iter().enumerate() { self.objs.enemy_attack_display[i].update(attack, obj); } let mut actions_to_apply = vec![]; // update the dice display to display the current values for ((die_obj, (current_face, cooldown)), cooldown_healthbar) in self .objs .dice .iter_mut() .zip(current_battle_state.rolled_dice.faces_to_render()) .zip(self.objs.dice_cooldowns.iter_mut()) { die_obj.set_sprite(obj.sprite(FACE_SPRITES.sprite_for_face(current_face))); if let Some(cooldown) = cooldown { cooldown_healthbar .set_value((cooldown * 24 / MALFUNCTION_COOLDOWN_FRAMES) as usize, obj); cooldown_healthbar.show(); } else { cooldown_healthbar.hide(); } } let mut animations_to_remove = vec![]; for (i, animation) in self.animations.iter_mut().enumerate() { match animation.update(&mut self.objs, obj, current_battle_state) { AnimationUpdateState::RemoveWithAction(a) => { actions_to_apply.push(a); animations_to_remove.push(i); } AnimationUpdateState::Continue => {} } } for &animation_to_remove in animations_to_remove.iter().rev() { self.animations.swap_remove(animation_to_remove); } actions_to_apply } pub fn add_action(&mut self, action: Action, obj: &'a OamManaged, sfx: &mut Sfx) { play_sound_for_action_start(&action, sfx); self.animations .push(AnimationStateHolder::for_action(action, obj)); } } fn play_sound_for_action_start(action: &Action, sfx: &mut Sfx) { match action { Action::PlayerShoot { .. } | Action::EnemyShoot { .. } => sfx.shoot(), _ => {} } } struct EnemyAttackDisplay<'a> { face: Object<'a>, cooldown: HealthBar<'a>, number: NumberDisplay<'a>, } impl<'a> EnemyAttackDisplay<'a> { pub fn new(face: Object<'a>, cooldown: HealthBar<'a>, number: NumberDisplay<'a>) -> Self { Self { face, cooldown, number, } } pub fn update(&mut self, attack: &Option, obj: &'a OamManaged) { if let Some(attack) = attack { self.face.show().set_sprite( obj.sprite(ENEMY_ATTACK_SPRITES.sprite_for_attack(attack.attack_type())), ); self.cooldown .set_value((attack.cooldown * 48 / attack.max_cooldown) as usize, obj); self.cooldown.show(); self.number.set_value(attack.value_to_show(), obj); } else { self.face.hide(); self.cooldown.hide(); self.number.set_value(None, obj); } } } enum AnimationState<'a> { PlayerShoot { bullet: Object<'a>, x: i32 }, PlayerActivateShield { amount: u32, frame: usize }, PlayerDisrupt { bullet: Object<'a>, x: i32 }, PlayerBurstShield { frame: usize }, PlayerSendBurstShield { bullet: Object<'a>, x: i32 }, PlayerHeal {}, EnemyShoot { bullet: Object<'a>, x: i32 }, EnemyShield { amount: u32, frame: usize }, EnemyHeal {}, } struct AnimationStateHolder<'a> { action: Action, state: AnimationState<'a>, } enum AnimationUpdateState { RemoveWithAction(Action), Continue, } impl<'a> AnimationStateHolder<'a> { fn for_action(a: Action, obj: &'a OamManaged) -> Self { let state = match a { Action::PlayerActivateShield { amount, .. } => { AnimationState::PlayerActivateShield { amount, frame: 0 } } Action::PlayerShoot { .. } => AnimationState::PlayerShoot { bullet: obj.object_sprite(BULLET_SPRITE), x: 64, }, Action::PlayerDisrupt { .. } => AnimationState::PlayerDisrupt { bullet: obj.object_sprite(DISRUPT_BULLET), x: 64, }, Action::PlayerHeal { .. } => AnimationState::PlayerHeal {}, Action::PlayerBurstShield { .. } => AnimationState::PlayerBurstShield { frame: 0 }, Action::PlayerSendBurstShield { .. } => AnimationState::PlayerSendBurstShield { bullet: obj.object_sprite(BURST_BULLET), x: 64, }, Action::EnemyShoot { .. } => AnimationState::EnemyShoot { bullet: obj.object_sprite(BULLET_SPRITE), x: 175, }, Action::EnemyShield { amount, .. } => AnimationState::EnemyShield { amount, frame: 0 }, Action::EnemyHeal { .. } => AnimationState::EnemyHeal {}, }; Self { action: a, state } } fn update( &mut self, objs: &mut BattleScreenDisplayObjects<'a>, obj: &'a OamManaged, current_battle_state: &CurrentBattleState, ) -> AnimationUpdateState { match &mut self.state { AnimationState::PlayerShoot { bullet, x } => { bullet.show().set_x(*x as u16).set_y(36); *x += 4; if *x > 180 { AnimationUpdateState::RemoveWithAction(self.action.clone()) } else { AnimationUpdateState::Continue } } AnimationState::PlayerDisrupt { bullet, x } => { bullet.show().set_x(*x as u16).set_y(36); *x += 2; if *x > 180 { AnimationUpdateState::RemoveWithAction(self.action.clone()) } else { AnimationUpdateState::Continue } } AnimationState::PlayerActivateShield { amount, frame } => { // find all the shields that need animating let current_player_shields = current_battle_state.player.shield_count; if current_player_shields < *amount { for i in current_player_shields..*amount { objs.player_shield[i as usize] .show() .set_sprite(obj.sprite(SHIELD.sprite(3 - *frame / 2))); } } else { return AnimationUpdateState::RemoveWithAction(self.action.clone()); } *frame += 1; if *frame >= 6 { AnimationUpdateState::RemoveWithAction(self.action.clone()) } else { AnimationUpdateState::Continue } } AnimationState::EnemyShoot { bullet, x } => { bullet.show().set_hflip(true).set_x(*x as u16).set_y(36); *x -= 4; if *x < 50 { AnimationUpdateState::RemoveWithAction(self.action.clone()) } else { AnimationUpdateState::Continue } } AnimationState::EnemyShield { amount, frame } => { // find all the shields that need animating let current_enemy_shields = current_battle_state.enemy.shield_count; if current_enemy_shields < *amount { for i in current_enemy_shields..*amount { objs.enemy_shield[i as usize] .show() .set_sprite(obj.sprite(SHIELD.sprite(3 - *frame / 2))); } } else { return AnimationUpdateState::RemoveWithAction(self.action.clone()); } *frame += 1; if *frame > 6 { AnimationUpdateState::RemoveWithAction(self.action.clone()) } else { AnimationUpdateState::Continue } } AnimationState::EnemyHeal {} => { AnimationUpdateState::RemoveWithAction(self.action.clone()) // TODO: Animation for healing } AnimationState::PlayerHeal {} => { AnimationUpdateState::RemoveWithAction(self.action.clone()) // TODO: Animation for healing } AnimationState::PlayerBurstShield { frame } => { if *frame < 10 { for shield in objs.player_shield.iter_mut() { shield.set_sprite(obj.sprite(SHIELD.sprite(*frame / 2))); } *frame += 1; AnimationUpdateState::Continue } else { for shield in objs.player_shield.iter_mut() { shield.set_sprite(obj.sprite(SHIELD.sprite(0))); } AnimationUpdateState::RemoveWithAction(self.action.clone()) } } AnimationState::PlayerSendBurstShield { bullet, x } => { bullet.show().set_x(*x as u16).set_y(36); *x += 1; if *x > 180 { AnimationUpdateState::RemoveWithAction(self.action.clone()) } else { AnimationUpdateState::Continue } } } } }