agb/examples/the-hat-chooses-the-wizard/src/lib.rs

988 lines
31 KiB
Rust
Raw Normal View History

2022-12-02 04:16:53 +11:00
#![no_std]
#![no_main]
#![cfg_attr(test, feature(custom_test_frameworks))]
#![cfg_attr(test, reexport_test_harness_main = "test_main")]
#![cfg_attr(test, test_runner(agb::test_runner::test_runner))]
extern crate alloc;
use agb::{
display::{
object::{Graphics, Object, ObjectController, Tag, TagMap},
tiled::{
InfiniteScrolledMap, PartialUpdateStatus, RegularBackgroundSize, TileFormat, TileSet,
TileSetting, TiledMap, VRamManager,
},
Priority, HEIGHT, WIDTH,
},
fixnum::{FixedNum, Vector2D},
input::{self, Button, ButtonController},
sound::mixer::Frequency,
};
use alloc::boxed::Box;
mod enemies;
mod level_display;
mod sfx;
mod splash_screen;
pub struct Level {
background: &'static [u16],
foreground: &'static [u16],
dimensions: Vector2D<u32>,
collision: &'static [u32],
slimes: &'static [(i32, i32)],
snails: &'static [(i32, i32)],
enemy_stops: &'static [(i32, i32)],
start_pos: (i32, i32),
}
mod map_tiles {
use super::Level;
pub const LEVELS: &[Level] = &[
l1_1::get_level(),
l1_2::get_level(),
l1_3::get_level(),
l1_4::get_level(),
l1_5::get_level(),
l1_7::get_level(), // these are intentionally this way round
l1_6::get_level(),
l1_8::get_level(),
l2_3::get_level(), // goes 2-3, 2-1 then 2-2
l2_1::get_level(),
l2_2::get_level(),
l2_4::get_level(),
];
pub mod l1_1 {
include!(concat!(env!("OUT_DIR"), "/1-1.json.rs"));
}
pub mod l1_2 {
include!(concat!(env!("OUT_DIR"), "/1-2.json.rs"));
}
pub mod l1_3 {
include!(concat!(env!("OUT_DIR"), "/1-3.json.rs"));
}
pub mod l1_4 {
include!(concat!(env!("OUT_DIR"), "/1-4.json.rs"));
}
pub mod l1_5 {
include!(concat!(env!("OUT_DIR"), "/1-5.json.rs"));
}
pub mod l1_6 {
include!(concat!(env!("OUT_DIR"), "/1-6.json.rs"));
}
pub mod l1_7 {
include!(concat!(env!("OUT_DIR"), "/1-7.json.rs"));
}
pub mod l2_1 {
include!(concat!(env!("OUT_DIR"), "/2-1.json.rs"));
}
pub mod l1_8 {
include!(concat!(env!("OUT_DIR"), "/1-8.json.rs"));
}
pub mod l2_2 {
include!(concat!(env!("OUT_DIR"), "/2-2.json.rs"));
}
pub mod l2_3 {
include!(concat!(env!("OUT_DIR"), "/2-3.json.rs"));
}
pub mod l2_4 {
include!(concat!(env!("OUT_DIR"), "/2-4.json.rs"));
}
pub mod tilemap {
include!(concat!(env!("OUT_DIR"), "/tilemap.rs"));
}
}
agb::include_gfx!("gfx/tile_sheet.toml");
const GRAPHICS: &Graphics = agb::include_aseprite!("gfx/sprites.aseprite");
const TAG_MAP: &TagMap = GRAPHICS.tags();
const WALKING: &Tag = TAG_MAP.get("Walking");
const JUMPING: &Tag = TAG_MAP.get("Jumping");
const FALLING: &Tag = TAG_MAP.get("Falling");
const PLAYER_DEATH: &Tag = TAG_MAP.get("Player Death");
const HAT_SPIN_1: &Tag = TAG_MAP.get("HatSpin");
const HAT_SPIN_2: &Tag = TAG_MAP.get("HatSpin2");
const HAT_SPIN_3: &Tag = TAG_MAP.get("HatSpin3");
type FixedNumberType = FixedNum<10>;
pub struct Entity<'a> {
sprite: Object<'a>,
position: Vector2D<FixedNumberType>,
velocity: Vector2D<FixedNumberType>,
collision_mask: Vector2D<u16>,
}
impl<'a> Entity<'a> {
pub fn new(object: &'a ObjectController, collision_mask: Vector2D<u16>) -> Self {
let dummy_sprite = object.sprite(WALKING.sprite(0));
let mut sprite = object.object(dummy_sprite);
sprite.set_priority(Priority::P1);
Entity {
sprite,
collision_mask,
position: (0, 0).into(),
velocity: (0, 0).into(),
}
}
fn something_at_point<T: Fn(i32, i32) -> bool>(
&self,
position: Vector2D<FixedNumberType>,
something_fn: T,
) -> bool {
let left = (position.x - self.collision_mask.x as i32 / 2).floor() / 8;
let right = (position.x + self.collision_mask.x as i32 / 2 - 1).floor() / 8;
let top = (position.y - self.collision_mask.y as i32 / 2).floor() / 8;
let bottom = (position.y + self.collision_mask.y as i32 / 2 - 1).floor() / 8;
for x in left..=right {
for y in top..=bottom {
if something_fn(x, y) {
return true;
}
}
}
false
}
fn collision_at_point(&self, level: &Level, position: Vector2D<FixedNumberType>) -> bool {
self.something_at_point(position, |x, y| level.collides(x, y))
}
fn killision_at_point(&self, level: &Level, position: Vector2D<FixedNumberType>) -> bool {
self.something_at_point(position, |x, y| level.kills(x, y))
}
fn completion_at_point(&self, level: &Level, position: Vector2D<FixedNumberType>) -> bool {
self.something_at_point(position, |x, y| level.wins(x, y))
}
fn enemy_collision_at_point(
&self,
enemies: &[enemies::Enemy],
position: Vector2D<FixedNumberType>,
) -> bool {
for enemy in enemies {
if enemy.collides_with_hat(position) {
return true;
}
}
false
}
// returns the distance actually moved
fn update_position(&mut self, level: &Level) -> Vector2D<FixedNumberType> {
let old_position = self.position;
let x_velocity = (self.velocity.x, 0.into()).into();
if !self.collision_at_point(level, self.position + x_velocity) {
self.position += x_velocity;
} else {
self.position += self.binary_search_collision(level, (1, 0).into(), self.velocity.x);
}
let y_velocity = (0.into(), self.velocity.y).into();
if !self.collision_at_point(level, self.position + y_velocity) {
self.position += y_velocity;
} else {
self.position += self.binary_search_collision(level, (0, 1).into(), self.velocity.y);
}
self.position - old_position
}
fn update_position_with_enemy(
&mut self,
level: &Level,
enemies: &[enemies::Enemy],
) -> (Vector2D<FixedNumberType>, bool) {
let mut was_enemy_collision = false;
let old_position = self.position;
let x_velocity = (self.velocity.x, 0.into()).into();
if !(self.collision_at_point(level, self.position + x_velocity)
|| self.enemy_collision_at_point(enemies, self.position + x_velocity))
{
self.position += x_velocity;
} else if self.enemy_collision_at_point(enemies, self.position + x_velocity) {
self.position -= x_velocity;
was_enemy_collision = true;
}
let y_velocity = (0.into(), self.velocity.y).into();
if !(self.collision_at_point(level, self.position + y_velocity)
|| self.enemy_collision_at_point(enemies, self.position + y_velocity))
{
self.position += y_velocity;
} else if self.enemy_collision_at_point(enemies, self.position + y_velocity) {
self.position -= y_velocity;
was_enemy_collision = true;
}
(self.position - old_position, was_enemy_collision)
}
fn binary_search_collision(
&self,
level: &Level,
unit_vector: Vector2D<FixedNumberType>,
initial: FixedNumberType,
) -> Vector2D<FixedNumberType> {
let mut low: FixedNumberType = 0.into();
let mut high = initial;
let one: FixedNumberType = 1.into();
while (high - low).abs() > one / 8 {
let mid = (low + high) / 2;
let new_vel: Vector2D<FixedNumberType> = unit_vector * mid;
if self.collision_at_point(level, self.position + new_vel) {
high = mid;
} else {
low = mid;
}
}
unit_vector * low
}
fn commit_position(&mut self, offset: Vector2D<FixedNumberType>) {
let position = (self.position - offset).floor();
self.sprite.set_position(position - (8, 8).into());
if position.x < -8 || position.x > WIDTH + 8 || position.y < -8 || position.y > HEIGHT + 8 {
self.sprite.hide();
} else {
self.sprite.show();
}
}
}
struct Map<'a, 'b> {
background: &'a mut InfiniteScrolledMap<'b>,
foreground: &'a mut InfiniteScrolledMap<'b>,
position: Vector2D<FixedNumberType>,
level: &'a Level,
}
impl<'a, 'b> Map<'a, 'b> {
pub fn commit_position(&mut self, vram: &mut VRamManager) {
self.background.set_pos(vram, self.position.floor());
self.foreground.set_pos(vram, self.position.floor());
self.background.commit(vram);
self.foreground.commit(vram);
}
pub fn init_background(&mut self, vram: &mut VRamManager) -> PartialUpdateStatus {
self.background.init_partial(vram, self.position.floor())
}
pub fn init_foreground(&mut self, vram: &mut VRamManager) -> PartialUpdateStatus {
self.foreground.init_partial(vram, self.position.floor())
}
}
impl Level {
fn collides(&self, x: i32, y: i32) -> bool {
self.at_point(x, y, map_tiles::tilemap::COLLISION_TILE as u32)
}
fn kills(&self, x: i32, y: i32) -> bool {
self.at_point(x, y, map_tiles::tilemap::KILL_TILE as u32)
}
fn at_point(&self, x: i32, y: i32, tile: u32) -> bool {
if (x < 0 || x >= self.dimensions.x as i32) || (y < 0 || y >= self.dimensions.y as i32) {
return true;
}
let pos = (self.dimensions.x as i32 * y + x) as usize;
let tile_foreground = self.foreground[pos];
let tile_background = self.background[pos];
let foreground_tile_property = self.collision[tile_foreground as usize];
let background_tile_property = self.collision[tile_background as usize];
foreground_tile_property == tile || background_tile_property == tile
}
fn wins(&self, x: i32, y: i32) -> bool {
self.at_point(x, y, map_tiles::tilemap::WIN_TILE as u32)
}
}
#[derive(PartialEq, Eq, Copy, Clone)]
pub enum HatState {
OnHead,
Thrown,
WizardTowards,
}
struct Player<'a> {
wizard: Entity<'a>,
hat: Entity<'a>,
hat_state: HatState,
hat_left_range: bool,
hat_slow_counter: i32,
wizard_frame: u8,
num_recalls: i8,
is_on_ground: bool,
facing: input::Tri,
}
fn ping_pong(i: i32, n: i32) -> i32 {
let cycle = 2 * (n - 1);
let i = i % cycle;
if i >= n {
cycle - i
} else {
i
}
}
impl<'a> Player<'a> {
fn new(controller: &'a ObjectController, start_position: Vector2D<FixedNumberType>) -> Self {
let mut wizard = Entity::new(controller, (6_u16, 14_u16).into());
let mut hat = Entity::new(controller, (6_u16, 6_u16).into());
wizard
.sprite
.set_sprite(controller.sprite(HAT_SPIN_1.sprite(0)));
hat.sprite
.set_sprite(controller.sprite(HAT_SPIN_1.sprite(0)));
wizard.sprite.show();
hat.sprite.show();
hat.sprite.set_z(-1);
wizard.position = start_position;
hat.position = start_position - (0, 10).into();
Player {
wizard,
hat,
hat_slow_counter: 0,
hat_state: HatState::OnHead,
hat_left_range: false,
wizard_frame: 0,
num_recalls: 0,
is_on_ground: true,
facing: input::Tri::Zero,
}
}
fn update_frame(
&mut self,
input: &ButtonController,
controller: &'a ObjectController,
timer: i32,
level: &Level,
enemies: &[enemies::Enemy],
sfx_player: &mut sfx::SfxPlayer,
) {
// throw or recall
if input.is_just_pressed(Button::A) {
if self.hat_state == HatState::OnHead {
let direction: Vector2D<FixedNumberType> = {
let up_down = input.y_tri() as i32;
let left_right = if up_down == 0 {
self.facing as i32
} else {
input.x_tri() as i32
};
(left_right, up_down).into()
};
if direction != (0, 0).into() {
let mut velocity = direction.normalise() * 5;
if velocity.y > 0.into() {
velocity.y *= FixedNumberType::new(4) / 3;
}
self.hat.velocity = velocity;
self.hat_state = HatState::Thrown;
sfx_player.throw();
}
} else if self.hat_state == HatState::Thrown {
self.num_recalls += 1;
if self.num_recalls < 3 {
self.hat.velocity = (0, 0).into();
self.wizard.velocity = (0, 0).into();
self.hat_state = HatState::WizardTowards;
}
} else if self.hat_state == HatState::WizardTowards {
self.hat_state = HatState::Thrown;
self.wizard.velocity /= 8;
}
}
let was_on_ground = self.is_on_ground;
let is_on_ground = self
.wizard
.collision_at_point(level, self.wizard.position + (0, 1).into());
if is_on_ground && !was_on_ground && self.wizard.velocity.y > 1.into() {
sfx_player.land();
}
self.is_on_ground = is_on_ground;
if self.hat_state != HatState::WizardTowards {
if is_on_ground {
self.num_recalls = 0;
}
if is_on_ground {
self.wizard.velocity.x += FixedNumberType::new(input.x_tri() as i32) / 16;
self.wizard.velocity = self.wizard.velocity * 54 / 64;
if input.is_just_pressed(Button::B) {
self.wizard.velocity.y = -FixedNumberType::new(3) / 2;
sfx_player.jump();
}
} else {
self.wizard.velocity.x += FixedNumberType::new(input.x_tri() as i32) / 64;
self.wizard.velocity = self.wizard.velocity * 63 / 64;
let gravity: Vector2D<FixedNumberType> = (0, 1).into();
let gravity = gravity / 16;
self.wizard.velocity += gravity;
}
self.wizard.velocity = self.wizard.update_position(level);
if self.wizard.velocity.x.abs() > 0.into() {
let offset = (ping_pong(timer / 16, 4)) as usize;
self.wizard_frame = offset as u8;
let frame = WALKING.animation_sprite(offset);
let sprite = controller.sprite(frame);
self.wizard.sprite.set_sprite(sprite);
}
if self.wizard.velocity.y < -FixedNumberType::new(1) / 16 {
// going up
self.wizard_frame = 5;
let frame = JUMPING.animation_sprite(0);
let sprite = controller.sprite(frame);
self.wizard.sprite.set_sprite(sprite);
} else if self.wizard.velocity.y > FixedNumberType::new(1) / 16 {
// going down
let offset = if self.wizard.velocity.y * 2 > 3.into() {
(timer / 4) as usize
} else {
// Don't flap beard unless going quickly
0
};
self.wizard_frame = 0;
let frame = FALLING.animation_sprite(offset);
let sprite = controller.sprite(frame);
self.wizard.sprite.set_sprite(sprite);
}
if input.x_tri() != agb::input::Tri::Zero {
self.facing = input.x_tri();
}
}
let hat_base_tile = match self.num_recalls {
0 => HAT_SPIN_1,
1 => HAT_SPIN_2,
_ => HAT_SPIN_3,
};
let hat_resting_position = match self.wizard_frame {
1 | 2 => (0, 9).into(),
5 => (0, 10).into(),
_ => (0, 8).into(),
};
match self.facing {
agb::input::Tri::Negative => {
self.wizard.sprite.set_hflip(true);
self.hat
.sprite
.set_sprite(controller.sprite(hat_base_tile.sprite(5)));
}
agb::input::Tri::Positive => {
self.wizard.sprite.set_hflip(false);
self.hat
.sprite
.set_sprite(controller.sprite(hat_base_tile.sprite(0)));
}
_ => {}
}
match self.hat_state {
HatState::Thrown => {
// hat is thrown, make hat move towards wizard
let distance_vector =
self.wizard.position - self.hat.position - hat_resting_position;
let distance = distance_vector.magnitude();
let direction = if distance == 0.into() {
(0, 0).into()
} else {
distance_vector / distance
};
let hat_sprite_divider = match self.num_recalls {
0 => 1,
1 => 2,
_ => 4,
};
let hat_sprite_offset = (timer / hat_sprite_divider) as usize;
self.hat.sprite.set_sprite(
controller.sprite(hat_base_tile.animation_sprite(hat_sprite_offset)),
);
if self.hat_slow_counter < 30 && self.hat.velocity.magnitude() < 2.into() {
self.hat.velocity = (0, 0).into();
self.hat_slow_counter += 1;
} else {
self.hat.velocity += direction / 4;
}
let (new_velocity, enemy_collision) =
self.hat.update_position_with_enemy(level, enemies);
self.hat.velocity = new_velocity;
if enemy_collision {
sfx_player.snail_hat_bounce();
}
if distance > 16.into() {
self.hat_left_range = true;
}
if self.hat_left_range && distance < 16.into() {
sfx_player.catch();
self.hat_state = HatState::OnHead;
}
}
HatState::OnHead => {
// hat is on head, place hat on head
self.hat_slow_counter = 0;
self.hat_left_range = false;
self.hat.position = self.wizard.position - hat_resting_position;
}
HatState::WizardTowards => {
self.hat.sprite.set_sprite(
controller.sprite(hat_base_tile.animation_sprite(timer as usize / 2)),
);
let distance_vector =
self.hat.position - self.wizard.position + hat_resting_position;
let distance = distance_vector.magnitude();
if distance != 0.into() {
let v = self.wizard.velocity.magnitude() + 1;
self.wizard.velocity = distance_vector / distance * v;
}
self.wizard.velocity = self.wizard.update_position(level);
if distance < 16.into() {
self.wizard.velocity /= 8;
self.hat_state = HatState::OnHead;
sfx_player.catch();
}
}
}
}
}
struct PlayingLevel<'a, 'b> {
timer: i32,
background: Map<'a, 'b>,
input: ButtonController,
player: Player<'a>,
enemies: [enemies::Enemy<'a>; 16],
}
enum UpdateState {
Normal,
Dead,
Complete,
}
impl<'a, 'b> PlayingLevel<'a, 'b> {
fn open_level(
level: &'a Level,
object_control: &'a ObjectController,
background: &'a mut InfiniteScrolledMap<'b>,
foreground: &'a mut InfiniteScrolledMap<'b>,
input: ButtonController,
) -> Self {
let mut e: [enemies::Enemy<'a>; 16] = Default::default();
let mut enemy_count = 0;
for &slime in level.slimes {
e[enemy_count] = enemies::Enemy::new_slime(object_control, slime.into());
enemy_count += 1;
}
for &snail in level.snails {
e[enemy_count] = enemies::Enemy::new_snail(object_control, snail.into());
enemy_count += 1;
}
let start_pos: Vector2D<FixedNumberType> = level.start_pos.into();
let background_position = (
(start_pos.x - WIDTH / 2)
.clamp(0.into(), ((level.dimensions.x * 8) as i32 - WIDTH).into()),
(start_pos.y - HEIGHT / 2)
.clamp(0.into(), ((level.dimensions.y * 8) as i32 - HEIGHT).into()),
)
.into();
PlayingLevel {
timer: 0,
background: Map {
background,
foreground,
level,
position: background_position,
},
player: Player::new(object_control, start_pos),
input,
enemies: e,
}
}
fn show_backgrounds(&mut self) {
self.background.background.show();
self.background.foreground.show();
}
fn hide_backgrounds(&mut self) {
self.background.background.hide();
self.background.foreground.hide();
}
fn clear_backgrounds(&mut self, vram: &mut VRamManager) {
self.background.background.clear(vram);
self.background.foreground.clear(vram);
}
fn dead_start(&mut self) {
self.player.wizard.velocity = (0, -1).into();
self.player.wizard.sprite.set_priority(Priority::P0);
}
fn dead_update(&mut self, controller: &'a ObjectController) -> bool {
self.timer += 1;
let frame = PLAYER_DEATH.animation_sprite(self.timer as usize / 8);
let sprite = controller.sprite(frame);
self.player.wizard.velocity += (0.into(), FixedNumberType::new(1) / 32).into();
self.player.wizard.position += self.player.wizard.velocity;
self.player.wizard.sprite.set_sprite(sprite);
self.player.wizard.commit_position(self.background.position);
self.player.wizard.position.y - self.background.position.y < (HEIGHT + 8).into()
}
fn update_frame(
&mut self,
sfx_player: &mut sfx::SfxPlayer,
vram: &mut VRamManager,
controller: &'a ObjectController,
) -> UpdateState {
self.timer += 1;
self.input.update();
let mut player_dead = false;
self.player.update_frame(
&self.input,
controller,
self.timer,
self.background.level,
&self.enemies,
sfx_player,
);
for enemy in self.enemies.iter_mut() {
match enemy.update(
controller,
self.background.level,
self.player.wizard.position,
self.player.hat_state,
self.timer,
sfx_player,
) {
enemies::EnemyUpdateState::KillPlayer => player_dead = true,
enemies::EnemyUpdateState::None => {}
}
}
self.background.position = self.get_next_map_position();
self.background.commit_position(vram);
self.player.wizard.commit_position(self.background.position);
self.player.hat.commit_position(self.background.position);
for enemy in self.enemies.iter_mut() {
enemy.commit(self.background.position);
}
player_dead |= self
.player
.wizard
.killision_at_point(self.background.level, self.player.wizard.position);
if player_dead {
UpdateState::Dead
} else if self
.player
.wizard
.completion_at_point(self.background.level, self.player.wizard.position)
{
UpdateState::Complete
} else {
UpdateState::Normal
}
}
fn get_next_map_position(&self) -> Vector2D<FixedNumberType> {
// want to ensure the player and the hat are visible if possible, so try to position the map
// so the centre is at the average position. But give the player some extra priority
let hat_pos = self.player.hat.position.floor();
let player_pos = self.player.wizard.position.floor();
let new_target_position = (hat_pos + player_pos * 3) / 4;
let screen: Vector2D<i32> = (WIDTH, HEIGHT).into();
let half_screen = screen / 2;
let current_centre = self.background.position.floor() + half_screen;
let mut target_position = ((current_centre * 3 + new_target_position) / 4) - half_screen;
target_position.x = target_position.x.clamp(
0,
(self.background.level.dimensions.x * 8 - (WIDTH as u32)) as i32,
);
target_position.y = target_position.y.clamp(
0,
(self.background.level.dimensions.y * 8 - (HEIGHT as u32)) as i32,
);
target_position.into()
}
}
pub fn main(mut agb: agb::Gba) -> ! {
let (tiled, mut vram) = agb.display.video.tiled0();
vram.set_background_palettes(tile_sheet::PALETTES);
let mut splash_screen = tiled.background(Priority::P0, RegularBackgroundSize::Background32x32);
let mut world_display = tiled.background(Priority::P0, RegularBackgroundSize::Background32x32);
let tileset = TileSet::new(tile_sheet::background.tiles, TileFormat::FourBpp);
for y in 0..32u16 {
for x in 0..32u16 {
world_display.set_tile(
&mut vram,
(x, y).into(),
&tileset,
TileSetting::from_raw(level_display::BLANK),
);
}
}
world_display.commit(&mut vram);
world_display.show();
splash_screen::show_splash_screen(
splash_screen::SplashScreen::Start,
None,
None,
&mut splash_screen,
&mut vram,
);
loop {
world_display.commit(&mut vram);
world_display.show();
vram.set_background_palettes(tile_sheet::PALETTES);
let object = agb.display.object.get();
let mut mixer = agb.mixer.mixer(Frequency::Hz10512);
mixer.enable();
let mut music_box = sfx::MusicBox::new();
let vblank = agb::interrupt::VBlank::get();
let mut current_level = 0;
loop {
if current_level == map_tiles::LEVELS.len() as u32 {
break;
}
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
level_display::write_level(
&mut world_display,
current_level / 8 + 1,
current_level % 8 + 1,
&tileset,
&mut vram,
);
world_display.commit(&mut vram);
world_display.show();
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
let map_current_level = current_level;
let mut background = InfiniteScrolledMap::new(
tiled.background(Priority::P2, RegularBackgroundSize::Background32x64),
Box::new(|pos: Vector2D<i32>| {
let level = &map_tiles::LEVELS[map_current_level as usize];
(
&tileset,
TileSetting::from_raw(
*level
.background
.get((pos.y * level.dimensions.x as i32 + pos.x) as usize)
.unwrap_or(&0),
),
)
}),
);
let mut foreground = InfiniteScrolledMap::new(
tiled.background(Priority::P0, RegularBackgroundSize::Background64x32),
Box::new(|pos: Vector2D<i32>| {
let level = &map_tiles::LEVELS[map_current_level as usize];
(
&tileset,
TileSetting::from_raw(
*level
.foreground
.get((pos.y * level.dimensions.x as i32 + pos.x) as usize)
.unwrap_or(&0),
),
)
}),
);
let mut level = PlayingLevel::open_level(
&map_tiles::LEVELS[current_level as usize],
&object,
&mut background,
&mut foreground,
agb::input::ButtonController::new(),
);
while level.background.init_background(&mut vram) != PartialUpdateStatus::Done {
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
}
while level.background.init_foreground(&mut vram) != PartialUpdateStatus::Done {
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
}
for _ in 0..20 {
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
}
object.commit();
level.show_backgrounds();
world_display.hide();
loop {
match level.update_frame(
&mut sfx::SfxPlayer::new(&mut mixer, &music_box),
&mut vram,
&object,
) {
UpdateState::Normal => {}
UpdateState::Dead => {
level.dead_start();
while level.dead_update(&object) {
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
object.commit();
}
break;
}
UpdateState::Complete => {
current_level += 1;
break;
}
}
music_box.before_frame(&mut mixer);
mixer.frame();
vblank.wait_for_vblank();
object.commit();
}
level.hide_backgrounds();
level.clear_backgrounds(&mut vram);
}
object.commit();
splash_screen::show_splash_screen(
splash_screen::SplashScreen::End,
Some(&mut mixer),
Some(&mut music_box),
&mut splash_screen,
&mut vram,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use agb::Gba;
#[test_case]
fn test_ping_pong(_gba: &mut Gba) {
let test_cases = [
[0, 2, 0],
[0, 7, 0],
[1, 2, 1],
[2, 2, 0],
[3, 2, 1],
[4, 2, 0],
];
for test_case in test_cases {
assert_eq!(
ping_pong(test_case[0], test_case[1]),
test_case[2],
"Expected ping_pong({}, {}) to equal {}",
test_case[0],
test_case[1],
test_case[2],
);
}
}
}