agb/examples/hyperspace-roll/src/battle.rs

617 lines
19 KiB
Rust

use crate::level_generation::generate_enemy_health;
use crate::sfx::Sfx;
use crate::{
graphics::SELECT_BOX, level_generation::generate_attack, Agb, EnemyAttackType, Face, PlayerDice,
};
use agb::display::tiled::{RegularMap, TiledMap};
use agb::{hash_map::HashMap, input::Button};
use alloc::vec;
use alloc::vec::Vec;
use self::display::BattleScreenDisplay;
mod display;
pub(super) const MALFUNCTION_COOLDOWN_FRAMES: u32 = 3 * 60;
const ROLL_TIME_FRAMES_ALL: u32 = 2 * 60;
const ROLL_TIME_FRAMES_ONE: u32 = 60 / 8;
/// A face of the rolled die and it's cooldown (should it be a malfunction)
#[derive(Debug)]
struct RolledDie {
face: Face,
cooldown: u32,
}
impl RolledDie {
fn new(face: Face) -> Self {
let cooldown = if face == Face::Malfunction {
MALFUNCTION_COOLDOWN_FRAMES
} else {
0
};
Self { face, cooldown }
}
fn update(&mut self) {
self.cooldown = self.cooldown.saturating_sub(1);
}
fn can_reroll(&self) -> bool {
self.face != Face::Malfunction || self.cooldown == 0
}
fn can_reroll_after_accept(&self) -> bool {
self.face != Face::Malfunction
}
fn cooldown(&self) -> Option<u32> {
if self.face == Face::Malfunction && self.cooldown > 0 {
Some(self.cooldown)
} else {
None
}
}
}
#[derive(Debug)]
enum DieState {
Rolling(u32, Face, Face),
Rolled(RolledDie),
}
#[derive(Debug, Clone)]
pub enum Action {
PlayerActivateShield { amount: u32 },
PlayerShoot { damage: u32, piercing: u32 },
PlayerDisrupt { amount: u32 },
PlayerHeal { amount: u32 },
PlayerBurstShield { multiplier: u32 },
PlayerSendBurstShield { damage: u32 },
EnemyShoot { damage: u32 },
EnemyShield { amount: u32 },
EnemyHeal { amount: u32 },
}
#[derive(Debug)]
struct RolledDice {
rolls: Vec<DieState>,
}
impl RolledDice {
fn update(&mut self, player_dice: &PlayerDice) {
self.rolls
.iter_mut()
.zip(player_dice.dice.iter())
.for_each(|(die_state, player_die)| match die_state {
DieState::Rolling(ref mut timeout, ref mut face, previous_face) => {
if *timeout == 0 {
let mut number_of_rolls = 0;
*die_state = DieState::Rolled(RolledDie::new(loop {
let next_face = player_die.roll();
number_of_rolls += 1;
if *previous_face != Face::Malfunction
|| next_face != *previous_face
|| number_of_rolls > 16
{
break next_face;
}
}));
} else {
if *timeout % 2 == 0 {
*face = player_die.roll();
}
*timeout -= 1;
}
}
DieState::Rolled(ref mut rolled_die) => rolled_die.update(),
});
}
fn faces_for_accepting(&self) -> impl Iterator<Item = Face> + '_ {
self.rolls.iter().filter_map(|state| match state {
DieState::Rolled(rolled_die) => Some(rolled_die.face),
_ => None,
})
}
fn faces_to_render(&self) -> impl Iterator<Item = (Face, Option<u32>)> + '_ {
self.rolls.iter().map(|rolled_die| match rolled_die {
DieState::Rolling(_, face, _previous_face) => (*face, None),
DieState::Rolled(rolled_die) => (rolled_die.face, rolled_die.cooldown()),
})
}
fn accept_rolls(&mut self, player_dice: &PlayerDice) -> Vec<Action> {
let mut actions = vec![];
let mut face_counts: HashMap<Face, u32> = HashMap::new();
let mut shield_multiplier = 1;
let mut shoot_multiplier = 1;
for face in self.faces_for_accepting() {
match face {
Face::DoubleShot => *face_counts.entry(Face::Shoot).or_default() += 2,
Face::TripleShot => *face_counts.entry(Face::Shoot).or_default() += 3,
Face::DoubleShield => *face_counts.entry(Face::Shield).or_default() += 2,
Face::TripleShield => *face_counts.entry(Face::Shield).or_default() += 3,
Face::DoubleShieldValue => shield_multiplier *= 2,
Face::DoubleShotValue => shoot_multiplier *= 2,
Face::TripleShotValue => shoot_multiplier *= 3,
other => *face_counts.entry(other).or_default() += 1,
}
}
let invert = *face_counts.entry(Face::Invert).or_default() % 2 == 1;
// shield
let mut shield_amount = *face_counts.entry(Face::Shield).or_default() * shield_multiplier;
// shooting
let shoot = *face_counts.entry(Face::Shoot).or_default();
let shoot_power = (shoot * (shoot + 1)) / 2;
let malfunction_shots = *face_counts.entry(Face::MalfunctionShot).or_default();
let malfunctions = *face_counts.entry(Face::Malfunction).or_default();
let malfunction_shoot = (malfunction_shots * (malfunction_shots + 1)) / 2
* (malfunctions * (malfunctions + 1))
/ 2;
if malfunction_shoot != 0 {
for roll in self.rolls.iter_mut().filter_map(|face| match face {
DieState::Rolled(rolled_die) if rolled_die.face == Face::Malfunction => {
Some(rolled_die)
}
_ => None,
}) {
roll.face = Face::Blank;
}
}
let mut shoot_power = (shoot_power + malfunction_shoot) * shoot_multiplier;
if invert {
(shoot_power, shield_amount) = (shield_amount, shoot_power);
}
if shoot_power > 0 {
actions.push(Action::PlayerShoot {
damage: shoot_power,
piercing: *face_counts.entry(Face::Bypass).or_default(),
});
}
if shield_amount > 0 {
actions.push(Action::PlayerActivateShield {
amount: shield_amount.min(5),
});
}
// burst shield
if face_counts.contains_key(&Face::BurstShield) {
actions.push(Action::PlayerBurstShield {
multiplier: shoot_multiplier,
});
}
// disrupt
let disrupt = *face_counts.entry(Face::Disrupt).or_default();
let disrupt_power = (disrupt * (disrupt + 1)) / 2;
if disrupt_power > 0 {
actions.push(Action::PlayerDisrupt {
amount: disrupt_power,
});
}
let heal = *face_counts.entry(Face::Heal).or_default();
if heal != 0 {
actions.push(Action::PlayerHeal {
amount: (heal * (heal + 1)) / 2,
});
}
let mut malfunction_all = false;
for roll in self.rolls.iter_mut().filter_map(|face| match face {
DieState::Rolled(rolled_die) => Some(rolled_die),
_ => None,
}) {
if roll.face == Face::DoubleShot
|| roll.face == Face::DoubleShield
|| roll.face == Face::DoubleShotValue
{
roll.cooldown = MALFUNCTION_COOLDOWN_FRAMES;
roll.face = Face::Malfunction;
}
if roll.face == Face::TripleShot
|| roll.face == Face::TripleShield
|| roll.face == Face::TripleShotValue
|| roll.face == Face::BurstShield
{
malfunction_all = true;
}
}
if malfunction_all {
for roll in self.rolls.iter_mut().filter_map(|face| match face {
DieState::Rolled(rolled_die) => Some(rolled_die),
_ => None,
}) {
roll.cooldown = MALFUNCTION_COOLDOWN_FRAMES;
roll.face = Face::Malfunction;
}
}
// reroll non-malfunctions after accepting
for i in 0..player_dice.dice.len() {
self.roll_die(i, ROLL_TIME_FRAMES_ALL, true, player_dice);
}
actions
}
fn roll_die(
&mut self,
die_index: usize,
time: u32,
is_after_accept: bool,
player_dice: &PlayerDice,
) {
if let DieState::Rolled(ref selected_rolled_die) = self.rolls[die_index] {
let can_reroll = if is_after_accept {
selected_rolled_die.can_reroll_after_accept()
} else {
selected_rolled_die.can_reroll()
};
if can_reroll {
self.rolls[die_index] = DieState::Rolling(
time,
player_dice.dice[die_index].roll(),
selected_rolled_die.face,
);
}
}
}
}
#[derive(Debug)]
struct PlayerState {
shield_count: u32,
health: u32,
max_health: u32,
}
#[derive(Debug)]
pub enum EnemyAttack {
Shoot(u32),
Shield(u32),
Heal(u32),
}
impl EnemyAttack {
fn apply_effect(&self) -> Action {
match self {
EnemyAttack::Shoot(damage) => Action::EnemyShoot { damage: *damage },
EnemyAttack::Shield(shield) => Action::EnemyShield { amount: *shield },
EnemyAttack::Heal(amount) => Action::EnemyHeal { amount: *amount },
}
}
}
#[derive(Debug)]
struct EnemyAttackState {
attack: EnemyAttack,
cooldown: u32,
max_cooldown: u32,
}
impl EnemyAttackState {
fn attack_type(&self) -> EnemyAttackType {
match self.attack {
EnemyAttack::Shoot(_) => EnemyAttackType::Attack,
EnemyAttack::Shield(_) => EnemyAttackType::Shield,
EnemyAttack::Heal(_) => EnemyAttackType::Heal,
}
}
fn value_to_show(&self) -> Option<u32> {
match self.attack {
EnemyAttack::Shoot(i) => Some(i),
EnemyAttack::Heal(i) => Some(i),
EnemyAttack::Shield(i) => Some(i),
}
}
#[must_use]
fn update(&mut self) -> Option<Action> {
if self.cooldown == 0 {
return Some(self.attack.apply_effect());
}
self.cooldown -= 1;
None
}
}
#[derive(Debug)]
struct EnemyState {
shield_count: u32,
health: u32,
max_health: u32,
}
#[derive(Debug)]
pub struct CurrentBattleState {
player: PlayerState,
enemy: EnemyState,
rolled_dice: RolledDice,
player_dice: PlayerDice,
attacks: [Option<EnemyAttackState>; 2],
current_level: u32,
}
impl CurrentBattleState {
fn accept_rolls(&mut self) -> Vec<Action> {
self.rolled_dice.accept_rolls(&self.player_dice)
}
fn roll_die(&mut self, die_index: usize, time: u32, is_after_accept: bool) {
self.rolled_dice
.roll_die(die_index, time, is_after_accept, &self.player_dice);
}
fn update(&mut self) -> Vec<Action> {
let mut actions = vec![];
for attack in self.attacks.iter_mut() {
if let Some(attack_state) = attack {
if let Some(action) = attack_state.update() {
attack.take();
actions.push(action);
}
} else if let Some(generated_attack) = generate_attack(self.current_level) {
attack.replace(EnemyAttackState {
attack: generated_attack.attack,
cooldown: generated_attack.cooldown,
max_cooldown: generated_attack.cooldown,
});
}
}
actions
}
fn update_dice(&mut self) {
self.rolled_dice.update(&self.player_dice);
}
fn apply_action(&mut self, action: Action, sfx: &mut Sfx) -> Option<Action> {
match action {
Action::PlayerActivateShield { amount } => {
if amount > self.player.shield_count {
sfx.shield_up();
}
self.player.shield_count = self.player.shield_count.max(amount);
None
}
Action::PlayerShoot { damage, piercing } => {
if self.enemy.shield_count <= piercing {
self.enemy.health = self.enemy.health.saturating_sub(damage);
sfx.shot_hit();
} else if self.enemy.shield_count <= damage {
self.enemy.shield_count = 0; // TODO: Dispatch action of drop shield to animate that
sfx.shield_down();
} else {
sfx.shield_defend();
}
None
}
Action::PlayerDisrupt { amount } => {
for attack in self.attacks.iter_mut().flatten() {
attack.cooldown += amount * 240;
attack.max_cooldown = attack.cooldown.max(attack.max_cooldown);
}
sfx.disrupt();
None
}
Action::PlayerHeal { amount } => {
self.player.health = self.player.max_health.min(self.player.health + amount);
sfx.heal();
None
}
Action::EnemyShoot { damage } => {
if self.player.shield_count == 0 {
self.player.health = self.player.health.saturating_sub(damage);
sfx.shot_hit();
} else if self.player.shield_count <= damage {
self.player.shield_count = 0; // TODO: Dispatch action of drop shield to animate that
sfx.shield_down();
} else {
sfx.shield_defend();
}
None
}
Action::EnemyShield { amount } => {
if amount > self.enemy.shield_count {
sfx.shield_up();
}
self.enemy.shield_count = self.enemy.shield_count.max(amount);
None
}
Action::EnemyHeal { amount } => {
self.enemy.health = self.enemy.max_health.min(self.enemy.health + amount);
sfx.heal();
None
}
Action::PlayerBurstShield { multiplier } => {
let damage =
self.player.shield_count * (self.player.shield_count + 1) * multiplier / 2;
self.player.shield_count = 0;
sfx.send_burst_shield();
Some(Action::PlayerSendBurstShield { damage })
}
Action::PlayerSendBurstShield { damage } => {
self.enemy.shield_count = 0;
self.enemy.health = self.enemy.health.saturating_sub(damage);
sfx.burst_shield_hit();
sfx.shield_down();
None
}
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum BattleResult {
Win,
Loss,
}
pub(crate) fn battle_screen(
agb: &mut Agb,
player_dice: PlayerDice,
current_level: u32,
help_background: &mut RegularMap,
) -> BattleResult {
agb.sfx.battle();
agb.sfx.frame();
help_background.set_scroll_pos((-16i16, -97i16).into());
crate::background::load_help_text(&mut agb.vram, help_background, 1, (0, 0));
crate::background::load_help_text(&mut agb.vram, help_background, 2, (0, 1));
let obj = &agb.obj;
let mut select_box_obj = agb.obj.object_sprite(SELECT_BOX.sprite(0));
select_box_obj.show();
let num_dice = player_dice.dice.len();
let enemy_health = generate_enemy_health(current_level);
let mut current_battle_state = CurrentBattleState {
player: PlayerState {
shield_count: 0,
health: 20,
max_health: 20,
},
enemy: EnemyState {
shield_count: 0,
health: enemy_health,
max_health: enemy_health,
},
rolled_dice: RolledDice {
rolls: player_dice
.dice
.iter()
.map(|die| DieState::Rolling(ROLL_TIME_FRAMES_ALL, die.roll(), Face::Blank))
.collect(),
},
player_dice: player_dice.clone(),
attacks: [None, None],
current_level,
};
let mut battle_screen_display = BattleScreenDisplay::new(obj, &current_battle_state);
agb.sfx.frame();
let mut selected_die = 0usize;
let mut input = agb::input::ButtonController::new();
let mut counter = 0usize;
loop {
counter = counter.wrapping_add(1);
for action_to_apply in battle_screen_display.update(obj, &current_battle_state) {
if let Some(action_to_return) =
current_battle_state.apply_action(action_to_apply, &mut agb.sfx)
{
battle_screen_display.add_action(action_to_return, obj, &mut agb.sfx);
}
}
for action in current_battle_state.update() {
battle_screen_display.add_action(action, obj, &mut agb.sfx);
}
current_battle_state.update_dice();
input.update();
if input.is_just_pressed(Button::LEFT) {
if selected_die == 0 {
selected_die = num_dice - 1;
} else {
selected_die -= 1;
}
agb.sfx.move_cursor();
}
if input.is_just_pressed(Button::RIGHT) {
if selected_die == num_dice - 1 {
selected_die = 0;
} else {
selected_die += 1;
}
agb.sfx.move_cursor();
}
if input.is_just_pressed(Button::A) {
current_battle_state.roll_die(selected_die, ROLL_TIME_FRAMES_ONE, false);
agb.sfx.roll();
}
if input.is_just_pressed(Button::START) {
for action in current_battle_state.accept_rolls() {
battle_screen_display.add_action(action, obj, &mut agb.sfx);
}
agb.sfx.roll_multi();
}
select_box_obj
.set_y(120 - 4)
.set_x(selected_die as u16 * 40 + 28 - 4)
.set_sprite(agb.obj.sprite(SELECT_BOX.animation_sprite(counter / 10)));
agb.star_background.update();
agb.sfx.frame();
agb.vblank.wait_for_vblank();
help_background.commit(&mut agb.vram);
help_background.show();
if current_battle_state.enemy.health == 0 {
agb.sfx.ship_explode();
help_background.hide();
crate::background::load_help_text(&mut agb.vram, help_background, 3, (0, 0));
crate::background::load_help_text(&mut agb.vram, help_background, 3, (0, 1));
return BattleResult::Win;
}
if current_battle_state.player.health == 0 {
agb.sfx.ship_explode();
help_background.hide();
crate::background::load_help_text(&mut agb.vram, help_background, 3, (0, 0));
crate::background::load_help_text(&mut agb.vram, help_background, 3, (0, 1));
return BattleResult::Loss;
}
agb.obj.commit();
agb.star_background.commit(&mut agb.vram);
}
}