2023-04-26 05:32:28 +10: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))]
|
2023-04-26 06:01:04 +10:00
|
|
|
#![deny(clippy::all)]
|
2023-04-26 05:32:28 +10:00
|
|
|
|
|
|
|
extern crate alloc;
|
|
|
|
|
2023-07-18 02:38:34 +10:00
|
|
|
use core::ops::Range;
|
|
|
|
|
2023-04-26 05:32:28 +10:00
|
|
|
use agb::{
|
|
|
|
display::{
|
|
|
|
self,
|
2023-04-26 06:00:26 +10:00
|
|
|
affine::AffineMatrix,
|
2023-04-26 05:32:28 +10:00
|
|
|
object::{
|
|
|
|
AffineMatrixInstance, AffineMode, Graphics, OamIterator, ObjectUnmanaged, Sprite,
|
2023-04-26 06:00:26 +10:00
|
|
|
SpriteLoader, SpriteVram, Tag,
|
2023-04-26 05:32:28 +10:00
|
|
|
},
|
|
|
|
palette16::Palette16,
|
|
|
|
},
|
|
|
|
fixnum::{num, Num, Vector2D},
|
|
|
|
include_aseprite,
|
|
|
|
input::{Button, ButtonController},
|
|
|
|
rng,
|
|
|
|
};
|
|
|
|
use alloc::{boxed::Box, collections::VecDeque, vec::Vec};
|
|
|
|
|
|
|
|
type Number = Num<i32, 8>;
|
|
|
|
|
|
|
|
struct Saw {
|
|
|
|
object: ObjectUnmanaged,
|
|
|
|
position: Vector2D<Number>,
|
|
|
|
angle: Number,
|
|
|
|
rotation_speed: Number,
|
|
|
|
}
|
|
|
|
|
2023-07-18 02:38:34 +10:00
|
|
|
#[derive(Clone, Copy)]
|
2023-04-26 05:32:28 +10:00
|
|
|
enum Colour {
|
|
|
|
Red,
|
|
|
|
Blue,
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Circle {
|
|
|
|
colour: Colour,
|
|
|
|
position: Vector2D<Number>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
struct SpriteCache {
|
|
|
|
saw: SpriteVram,
|
|
|
|
blue: SpriteVram,
|
|
|
|
red: SpriteVram,
|
|
|
|
numbers: Box<[SpriteVram]>,
|
2023-07-18 02:38:34 +10:00
|
|
|
bars: [Box<[SpriteVram]>; 2],
|
2023-04-26 05:32:28 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
|
|
enum DrawDirection {
|
|
|
|
Left,
|
|
|
|
Right,
|
|
|
|
}
|
|
|
|
|
2023-07-18 02:38:34 +10:00
|
|
|
fn draw_bar(
|
|
|
|
position: Vector2D<i32>,
|
|
|
|
length: usize,
|
|
|
|
colour: Colour,
|
|
|
|
oam: &mut OamIterator,
|
|
|
|
sprite_cache: &SpriteCache,
|
|
|
|
) -> Option<()> {
|
|
|
|
let length = length as i32;
|
|
|
|
let number_of_sprites = length / 8;
|
|
|
|
let size_of_last = length % 8;
|
|
|
|
|
|
|
|
let sprites = match colour {
|
|
|
|
Colour::Red => &sprite_cache.bars[0],
|
|
|
|
Colour::Blue => &sprite_cache.bars[1],
|
|
|
|
};
|
|
|
|
|
|
|
|
for sprite_idx in 0..number_of_sprites {
|
|
|
|
let mut object = ObjectUnmanaged::new(sprites[0].clone());
|
|
|
|
object
|
|
|
|
.show()
|
|
|
|
.set_position(position + (sprite_idx * 8, 0).into());
|
|
|
|
oam.next()?.set(&object);
|
|
|
|
}
|
|
|
|
|
|
|
|
if size_of_last != 0 {
|
|
|
|
let mut object = ObjectUnmanaged::new(sprites[8 - size_of_last as usize].clone());
|
|
|
|
object
|
|
|
|
.show()
|
|
|
|
.set_position(position + (number_of_sprites * 8, 0).into());
|
|
|
|
oam.next()?.set(&object);
|
|
|
|
}
|
|
|
|
|
|
|
|
Some(())
|
|
|
|
}
|
|
|
|
|
2023-04-26 05:32:28 +10:00
|
|
|
fn draw_number(
|
|
|
|
mut number: u32,
|
|
|
|
position: Vector2D<i32>,
|
|
|
|
oam: &mut OamIterator,
|
|
|
|
direction: DrawDirection,
|
|
|
|
sprite_cache: &SpriteCache,
|
2023-04-26 06:07:31 +10:00
|
|
|
) -> Option<()> {
|
2023-04-26 05:32:28 +10:00
|
|
|
let mut digits = Vec::new();
|
|
|
|
if number == 0 {
|
|
|
|
digits.push(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
while number != 0 {
|
|
|
|
digits.push(number % 10);
|
|
|
|
number /= 10;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut current_position = if direction == DrawDirection::Right {
|
|
|
|
position + (4 * (digits.len() - 1) as i32, 0).into()
|
|
|
|
} else {
|
|
|
|
position
|
|
|
|
};
|
|
|
|
|
|
|
|
for digit in digits {
|
|
|
|
let mut obj = ObjectUnmanaged::new(sprite_cache.numbers[digit as usize].clone());
|
|
|
|
obj.show().set_position(current_position);
|
|
|
|
|
2023-04-26 06:07:31 +10:00
|
|
|
oam.next()?.set(&obj);
|
2023-04-26 05:32:28 +10:00
|
|
|
|
|
|
|
current_position -= (4, 0).into();
|
|
|
|
}
|
2023-04-26 06:07:31 +10:00
|
|
|
|
|
|
|
Some(())
|
2023-04-26 05:32:28 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
impl SpriteCache {
|
|
|
|
fn new(loader: &mut SpriteLoader) -> Self {
|
|
|
|
const SPRITES: &Graphics = include_aseprite!(
|
|
|
|
"gfx/circles.aseprite",
|
|
|
|
"gfx/saw.aseprite",
|
2023-07-18 02:38:34 +10:00
|
|
|
"gfx/numbers.aseprite",
|
|
|
|
"gfx/bar.aseprite"
|
2023-04-26 05:32:28 +10:00
|
|
|
);
|
|
|
|
|
2023-07-18 02:38:34 +10:00
|
|
|
fn generate_sprites(
|
|
|
|
tag: &'static Tag,
|
|
|
|
range: Range<usize>,
|
|
|
|
loader: &mut SpriteLoader,
|
|
|
|
) -> Box<[SpriteVram]> {
|
|
|
|
range
|
|
|
|
.map(|x| tag.sprite(x))
|
|
|
|
.map(|x| loader.get_vram_sprite(x))
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
.into_boxed_slice()
|
|
|
|
}
|
|
|
|
|
2023-04-26 05:32:28 +10:00
|
|
|
const NUMBERS: &Tag = SPRITES.tags().get("numbers");
|
|
|
|
const BLUE_CIRCLE: &Sprite = SPRITES.tags().get("Blue").sprite(0);
|
|
|
|
const RED_CIRCLE: &Sprite = SPRITES.tags().get("Red").sprite(0);
|
|
|
|
const SAW: &Sprite = SPRITES.tags().get("Saw").sprite(0);
|
2023-07-18 02:38:34 +10:00
|
|
|
const BAR_RED: &Tag = SPRITES.tags().get("Red Bar");
|
|
|
|
const BAR_BLUE: &Tag = SPRITES.tags().get("Blue Bar");
|
2023-04-26 05:32:28 +10:00
|
|
|
|
|
|
|
Self {
|
|
|
|
saw: loader.get_vram_sprite(SAW),
|
|
|
|
blue: loader.get_vram_sprite(BLUE_CIRCLE),
|
|
|
|
red: loader.get_vram_sprite(RED_CIRCLE),
|
2023-07-18 02:38:34 +10:00
|
|
|
numbers: generate_sprites(NUMBERS, 0..10, loader),
|
|
|
|
bars: [
|
|
|
|
generate_sprites(BAR_RED, 0..8, loader),
|
|
|
|
generate_sprites(BAR_BLUE, 0..8, loader),
|
|
|
|
],
|
2023-04-26 05:32:28 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Game {
|
|
|
|
settings: FinalisedSettings,
|
|
|
|
circles: VecDeque<Circle>,
|
|
|
|
saws: VecDeque<Saw>,
|
|
|
|
head_position: Vector2D<Number>,
|
|
|
|
phase_time: Number,
|
|
|
|
input: ButtonController,
|
|
|
|
frame_since_last_saw: i32,
|
|
|
|
alive_frames: u32,
|
2023-07-18 02:38:34 +10:00
|
|
|
energy: Number,
|
2023-04-26 05:32:28 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
enum GameState {
|
|
|
|
Continue,
|
|
|
|
Loss(u32),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Game {
|
|
|
|
fn from_settings(settings: Settings) -> Self {
|
|
|
|
let finalised = settings.to_finalised_settings();
|
|
|
|
|
|
|
|
let mut circles = VecDeque::with_capacity(finalised.number_of_circles);
|
|
|
|
for idx in 0..finalised.number_of_circles {
|
|
|
|
circles.push_back(Circle {
|
|
|
|
colour: Colour::Red,
|
|
|
|
position: Vector2D::new(
|
|
|
|
finalised.speed * idx as i32 - 4,
|
|
|
|
settings.head_start_position.y,
|
|
|
|
),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
Game {
|
|
|
|
input: agb::input::ButtonController::new(),
|
2023-07-18 02:38:34 +10:00
|
|
|
energy: finalised.max_energy,
|
2023-04-26 05:32:28 +10:00
|
|
|
settings: finalised,
|
|
|
|
circles,
|
|
|
|
saws: VecDeque::new(),
|
|
|
|
head_position: settings.head_start_position,
|
|
|
|
phase_time: 0.into(),
|
|
|
|
frame_since_last_saw: 0,
|
|
|
|
alive_frames: 0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn frame(&mut self, sprite_cache: &SpriteCache) -> GameState {
|
|
|
|
self.input.update();
|
|
|
|
|
2023-07-18 02:38:34 +10:00
|
|
|
let (height, colour) = if self.input.is_pressed(Button::A) && self.energy > 0.into() {
|
|
|
|
self.energy -= self.settings.energy_use_speed;
|
2023-04-26 05:32:28 +10:00
|
|
|
(self.settings.wave_height_ability, Colour::Blue)
|
|
|
|
} else {
|
2023-07-18 02:38:34 +10:00
|
|
|
if self.input.is_released(Button::A) {
|
|
|
|
self.energy += self.settings.energy_recover_speed;
|
|
|
|
self.energy = self.energy.min(self.settings.max_energy);
|
|
|
|
}
|
2023-04-26 05:32:28 +10:00
|
|
|
(self.settings.wave_height_normal, Colour::Red)
|
|
|
|
};
|
|
|
|
|
|
|
|
let next_phase_time = self.phase_time + self.settings.phase_speed;
|
|
|
|
|
|
|
|
let this_frame_y_delta = next_phase_time.cos() - self.phase_time.cos();
|
|
|
|
self.phase_time = next_phase_time % num!(1.);
|
|
|
|
let this_frame_y_delta = this_frame_y_delta * height;
|
|
|
|
self.head_position.y += this_frame_y_delta;
|
|
|
|
|
|
|
|
// update circles
|
|
|
|
for circle in self.circles.iter_mut() {
|
|
|
|
circle.position.x -= self.settings.speed;
|
|
|
|
}
|
|
|
|
|
|
|
|
self.circles.pop_front();
|
|
|
|
|
|
|
|
// generate circle
|
|
|
|
let circle = Circle {
|
|
|
|
colour,
|
|
|
|
position: self.head_position,
|
|
|
|
};
|
|
|
|
|
|
|
|
self.circles.push_back(circle);
|
|
|
|
|
|
|
|
// update saws + check for death
|
|
|
|
let mut saw_has_hit_head = false;
|
|
|
|
let mut number_of_saws_to_pop = 0;
|
|
|
|
for (idx, saw) in self.saws.iter_mut().enumerate() {
|
|
|
|
saw.position.x -= self.settings.speed;
|
|
|
|
if saw.position.x < (-32).into() {
|
|
|
|
number_of_saws_to_pop = idx + 1;
|
|
|
|
}
|
|
|
|
saw.angle += saw.rotation_speed;
|
|
|
|
|
|
|
|
let angle_affine_matrix = AffineMatrix::from_rotation(saw.angle);
|
|
|
|
|
|
|
|
saw.object.set_affine_matrix(AffineMatrixInstance::new(
|
|
|
|
angle_affine_matrix.to_object_wrapping(),
|
|
|
|
));
|
|
|
|
saw.object.show_affine(AffineMode::Affine);
|
|
|
|
|
|
|
|
saw.object
|
|
|
|
.set_position(saw.position.floor() - (16, 16).into());
|
|
|
|
|
|
|
|
if (saw.position - self.head_position).magnitude_squared()
|
|
|
|
< ((16 + 4) * (16 + 4)).into()
|
|
|
|
{
|
|
|
|
saw_has_hit_head = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// destroy saws
|
|
|
|
for _ in 0..number_of_saws_to_pop {
|
|
|
|
self.saws.pop_front();
|
|
|
|
}
|
|
|
|
|
|
|
|
// create saw
|
|
|
|
self.frame_since_last_saw -= 1;
|
|
|
|
if self.frame_since_last_saw <= 0 {
|
|
|
|
self.frame_since_last_saw = self.settings.frames_between_saws;
|
|
|
|
let mut rotation_direction = rng::gen().signum();
|
|
|
|
if rotation_direction == 0 {
|
|
|
|
rotation_direction = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
let rotation_magnitude =
|
|
|
|
Number::from_raw(rng::gen().abs() % (1 << 8)) % num!(0.02) + num!(0.005);
|
|
|
|
|
|
|
|
let rotation_speed = rotation_magnitude * rotation_direction;
|
|
|
|
let saw = Saw {
|
|
|
|
object: ObjectUnmanaged::new(sprite_cache.saw.clone()),
|
|
|
|
position: (300, rng::gen().rem_euclid(display::HEIGHT)).into(),
|
|
|
|
angle: 0.into(),
|
|
|
|
rotation_speed,
|
|
|
|
};
|
|
|
|
|
|
|
|
self.saws.push_back(saw);
|
|
|
|
}
|
|
|
|
|
|
|
|
self.alive_frames += 1;
|
|
|
|
|
|
|
|
let out_of_bounds_death = self.head_position.y.floor() < -4
|
|
|
|
|| (self.head_position.y + 1).floor() > display::HEIGHT + 4;
|
|
|
|
|
|
|
|
if saw_has_hit_head || out_of_bounds_death {
|
|
|
|
GameState::Loss(self.alive_frames)
|
|
|
|
} else {
|
|
|
|
GameState::Continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-26 06:07:31 +10:00
|
|
|
fn render(&self, oam: &mut OamIterator, sprite_cache: &SpriteCache) -> Option<()> {
|
2023-04-26 05:32:28 +10:00
|
|
|
for saw in self.saws.iter() {
|
2023-04-26 06:07:31 +10:00
|
|
|
oam.next()?.set(&saw.object);
|
2023-04-26 05:32:28 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
for circle in self.circles.iter() {
|
2023-04-26 06:07:31 +10:00
|
|
|
let mut object = ObjectUnmanaged::new(match circle.colour {
|
|
|
|
Colour::Red => sprite_cache.red.clone(),
|
|
|
|
Colour::Blue => sprite_cache.blue.clone(),
|
|
|
|
});
|
2023-04-26 05:32:28 +10:00
|
|
|
|
2023-04-26 06:07:31 +10:00
|
|
|
object
|
|
|
|
.show()
|
|
|
|
.set_position(circle.position.floor() - (4, 4).into());
|
2023-04-26 05:32:28 +10:00
|
|
|
|
2023-04-26 06:07:31 +10:00
|
|
|
oam.next()?.set(&object);
|
2023-04-26 05:32:28 +10:00
|
|
|
}
|
2023-04-26 06:07:31 +10:00
|
|
|
|
|
|
|
Some(())
|
2023-04-26 05:32:28 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Settings {
|
|
|
|
phase_speed: Number,
|
|
|
|
frames_between_saws: i32,
|
|
|
|
speed: Number,
|
|
|
|
head_start_position: Vector2D<Number>,
|
|
|
|
wave_height_normal: Number,
|
|
|
|
wave_height_ability: Number,
|
2023-07-18 02:38:34 +10:00
|
|
|
max_energy: Number,
|
|
|
|
energy_use_speed: Number,
|
|
|
|
energy_recover_speed: Number,
|
2023-04-26 05:32:28 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Settings {
|
|
|
|
fn to_finalised_settings(&self) -> FinalisedSettings {
|
|
|
|
FinalisedSettings {
|
|
|
|
number_of_circles: ((self.head_start_position.x + 4) / self.speed + 1)
|
|
|
|
.floor()
|
|
|
|
.try_into()
|
|
|
|
.expect("number should be positive"),
|
|
|
|
speed: self.speed,
|
|
|
|
phase_speed: self.phase_speed,
|
|
|
|
frames_between_saws: self.frames_between_saws,
|
|
|
|
wave_height_ability: self.wave_height_ability,
|
|
|
|
wave_height_normal: self.wave_height_normal,
|
2023-07-18 02:38:34 +10:00
|
|
|
max_energy: self.max_energy,
|
|
|
|
energy_recover_speed: self.energy_recover_speed,
|
|
|
|
energy_use_speed: self.energy_use_speed,
|
2023-04-26 05:32:28 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct FinalisedSettings {
|
|
|
|
wave_height_normal: Number,
|
|
|
|
wave_height_ability: Number,
|
|
|
|
phase_speed: Number,
|
|
|
|
frames_between_saws: i32,
|
|
|
|
speed: Number,
|
|
|
|
number_of_circles: usize,
|
2023-07-18 02:38:34 +10:00
|
|
|
max_energy: Number,
|
|
|
|
energy_use_speed: Number,
|
|
|
|
energy_recover_speed: Number,
|
2023-04-26 05:32:28 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn main(mut gba: agb::Gba) -> ! {
|
|
|
|
let (mut unmanaged, mut sprites) = gba.display.object.get_unmanaged();
|
|
|
|
let sprite_cache = SpriteCache::new(&mut sprites);
|
|
|
|
|
|
|
|
let (_background, mut vram) = gba.display.video.tiled0();
|
|
|
|
|
|
|
|
vram.set_background_palettes(&[Palette16::new([u16::MAX; 16])]);
|
|
|
|
|
|
|
|
let vblank = agb::interrupt::VBlank::get();
|
|
|
|
|
|
|
|
let mut max_score = 0;
|
|
|
|
|
|
|
|
loop {
|
|
|
|
let mut game = Game::from_settings(Settings {
|
|
|
|
phase_speed: num!(0.02),
|
|
|
|
frames_between_saws: 60,
|
|
|
|
speed: num!(1.),
|
|
|
|
head_start_position: (40, 100).into(),
|
|
|
|
wave_height_normal: 20.into(),
|
|
|
|
wave_height_ability: 5.into(),
|
2023-07-18 02:38:34 +10:00
|
|
|
max_energy: 128.into(),
|
|
|
|
energy_use_speed: num!(0.5),
|
|
|
|
energy_recover_speed: 0.into(),
|
2023-04-26 05:32:28 +10:00
|
|
|
});
|
|
|
|
loop {
|
|
|
|
let state = game.frame(&sprite_cache);
|
|
|
|
if game.alive_frames > max_score {
|
|
|
|
max_score = game.alive_frames;
|
|
|
|
}
|
2023-07-18 02:38:34 +10:00
|
|
|
let max_bar_width = display::WIDTH - 2;
|
|
|
|
let bar_width_pixels = (game.energy * max_bar_width) / game.settings.max_energy;
|
|
|
|
let bar_width_pixels = (bar_width_pixels + num!(0.5)).floor().max(0) as usize;
|
2023-04-26 05:32:28 +10:00
|
|
|
vblank.wait_for_vblank();
|
|
|
|
let oam_frame = &mut unmanaged.iter();
|
|
|
|
draw_number(
|
|
|
|
max_score,
|
2023-07-18 02:38:34 +10:00
|
|
|
(display::WIDTH - 5, 2).into(),
|
2023-04-26 05:32:28 +10:00
|
|
|
oam_frame,
|
|
|
|
DrawDirection::Left,
|
|
|
|
&sprite_cache,
|
|
|
|
);
|
|
|
|
draw_number(
|
|
|
|
game.alive_frames,
|
2023-07-18 02:38:34 +10:00
|
|
|
(2, 2).into(),
|
2023-04-26 05:32:28 +10:00
|
|
|
oam_frame,
|
|
|
|
DrawDirection::Right,
|
|
|
|
&sprite_cache,
|
|
|
|
);
|
2023-07-18 02:38:34 +10:00
|
|
|
draw_bar(
|
|
|
|
(1, 1).into(),
|
|
|
|
bar_width_pixels,
|
|
|
|
game.circles.back().unwrap().colour,
|
|
|
|
oam_frame,
|
|
|
|
&sprite_cache,
|
|
|
|
);
|
|
|
|
|
2023-04-26 05:32:28 +10:00
|
|
|
game.render(oam_frame, &sprite_cache);
|
|
|
|
|
2023-04-26 06:00:26 +10:00
|
|
|
if let GameState::Loss(_) = state {
|
2023-04-26 05:32:28 +10:00
|
|
|
for _ in 0..30 {
|
|
|
|
vblank.wait_for_vblank();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|