mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-24 16:51:33 +11:00
427 lines
12 KiB
Rust
427 lines
12 KiB
Rust
use agb::{
|
|
display::{
|
|
object::{
|
|
OamIterator, ObjectTextRender, ObjectUnmanaged, PaletteVram, Size, SpriteLoader,
|
|
SpriteVram, TextAlignment,
|
|
},
|
|
palette16::Palette16,
|
|
tiled::{MapLoan, RegularMap, TiledMap, VRamManager},
|
|
HEIGHT,
|
|
},
|
|
fixnum::Vector2D,
|
|
input::{Button, ButtonController, Tri},
|
|
};
|
|
|
|
use crate::{
|
|
resources::{ARROW_RIGHT, FONT},
|
|
sfx::Sfx,
|
|
};
|
|
|
|
use self::{
|
|
game_state::{GameState, PLAY_AREA_HEIGHT, PLAY_AREA_WIDTH},
|
|
simulation::Simulation,
|
|
};
|
|
|
|
pub use simulation::Direction;
|
|
|
|
use core::{cell::RefCell, fmt::Write};
|
|
|
|
mod game_state;
|
|
mod simulation;
|
|
|
|
mod numbers;
|
|
|
|
struct Game<'a, 'b> {
|
|
phase: GamePhase<'a, 'b>,
|
|
}
|
|
|
|
struct Lament<'a, 'b> {
|
|
level: usize,
|
|
writer: RefCell<ObjectTextRender<'static>>,
|
|
background: &'a mut MapLoan<'b, RegularMap>,
|
|
}
|
|
|
|
fn generate_text_palette() -> PaletteVram {
|
|
let mut palette = [0x0; 16];
|
|
palette[1] = 0xFF_FF;
|
|
let palette = Palette16::new(palette);
|
|
PaletteVram::new(&palette).unwrap()
|
|
}
|
|
|
|
impl<'a, 'b> Lament<'a, 'b> {
|
|
fn new(level: usize, background: &'a mut MapLoan<'b, RegularMap>) -> Self {
|
|
let palette = generate_text_palette();
|
|
|
|
let mut writer = ObjectTextRender::new(&super::resources::FONT, Size::S16x16, palette);
|
|
|
|
let _ = writeln!(
|
|
writer,
|
|
"{}\n\n{}",
|
|
numbers::NUMBERS[level],
|
|
crate::level::Level::get_level(level).name
|
|
);
|
|
|
|
writer.layout(
|
|
Vector2D::new(
|
|
PLAY_AREA_WIDTH as i32 * 16 - 32,
|
|
PLAY_AREA_HEIGHT as i32 * 16,
|
|
),
|
|
TextAlignment::Center,
|
|
0,
|
|
);
|
|
|
|
Self {
|
|
level,
|
|
writer: RefCell::new(writer),
|
|
background,
|
|
}
|
|
}
|
|
|
|
fn update(self, input: &ButtonController, vram_manager: &mut VRamManager) -> GamePhase<'a, 'b> {
|
|
{
|
|
let mut writer = self.writer.borrow_mut();
|
|
writer.next_letter_group();
|
|
writer.update(Vector2D::new(16, HEIGHT / 4));
|
|
}
|
|
if input.is_just_pressed(Button::A) {
|
|
GamePhase::Construction(Construction::new(self.level, self.background, vram_manager))
|
|
} else {
|
|
GamePhase::Lament(self)
|
|
}
|
|
}
|
|
|
|
fn render(&self, oam: &mut OamIterator) {
|
|
self.writer.borrow_mut().commit(oam);
|
|
}
|
|
}
|
|
|
|
struct Construction<'a, 'b> {
|
|
game: GameState,
|
|
background: &'a mut MapLoan<'b, RegularMap>,
|
|
}
|
|
|
|
impl<'a, 'b> Drop for Construction<'a, 'b> {
|
|
fn drop(&mut self) {
|
|
self.background.set_visible(false);
|
|
}
|
|
}
|
|
|
|
impl<'a, 'b> Construction<'a, 'b> {
|
|
fn new(
|
|
level: usize,
|
|
background: &'a mut MapLoan<'b, RegularMap>,
|
|
vram_manager: &mut VRamManager,
|
|
) -> Self {
|
|
let game = GameState::new(level);
|
|
game.load_level_background(background, vram_manager);
|
|
background.commit(vram_manager);
|
|
background.set_visible(true);
|
|
Self { background, game }
|
|
}
|
|
|
|
fn update(
|
|
mut self,
|
|
input: &ButtonController,
|
|
sfx: &mut Sfx,
|
|
loader: &mut SpriteLoader,
|
|
) -> GamePhase<'a, 'b> {
|
|
self.game.step(input, sfx);
|
|
if input.is_just_pressed(Button::START) {
|
|
self.game.force_place();
|
|
GamePhase::Execute(Execute::new(self, sfx, loader))
|
|
} else {
|
|
GamePhase::Construction(self)
|
|
}
|
|
}
|
|
|
|
fn render(&self, oam: &mut OamIterator, loader: &mut SpriteLoader) {
|
|
self.game.render(loader, oam);
|
|
}
|
|
}
|
|
|
|
impl<'a, 'b> Execute<'a, 'b> {
|
|
fn new(construction: Construction<'a, 'b>, sfx: &mut Sfx, loader: &mut SpriteLoader) -> Self {
|
|
Self {
|
|
simulation: construction.game.create_simulation(sfx, loader),
|
|
construction,
|
|
}
|
|
}
|
|
|
|
fn update(
|
|
mut self,
|
|
input: &ButtonController,
|
|
sfx: &mut Sfx,
|
|
loader: &mut SpriteLoader,
|
|
) -> GamePhase<'a, 'b> {
|
|
if input.is_just_pressed(Button::START) {
|
|
return GamePhase::Construction(self.construction);
|
|
}
|
|
|
|
match self.simulation.update(loader, sfx) {
|
|
simulation::Outcome::Continue => GamePhase::Execute(self),
|
|
simulation::Outcome::Loss => GamePhase::Construction(self.construction),
|
|
simulation::Outcome::Win => GamePhase::NextLevel,
|
|
}
|
|
}
|
|
|
|
fn render(&self, loader: &mut SpriteLoader, oam: &mut OamIterator) {
|
|
self.simulation.render(oam);
|
|
self.construction
|
|
.game
|
|
.render_arrows(loader, oam, Some(self.simulation.current_turn()));
|
|
}
|
|
}
|
|
|
|
struct Execute<'a, 'b> {
|
|
simulation: Simulation,
|
|
construction: Construction<'a, 'b>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
enum GamePhase<'a, 'b> {
|
|
#[default]
|
|
Empty,
|
|
Lament(Lament<'a, 'b>),
|
|
Construction(Construction<'a, 'b>),
|
|
Execute(Execute<'a, 'b>),
|
|
NextLevel,
|
|
}
|
|
|
|
impl GamePhase<'_, '_> {
|
|
fn update(
|
|
&mut self,
|
|
input: &ButtonController,
|
|
sfx: &mut Sfx,
|
|
loader: &mut SpriteLoader,
|
|
vram_manger: &mut VRamManager,
|
|
) {
|
|
*self = match core::mem::take(self) {
|
|
GamePhase::Lament(lament) => lament.update(input, vram_manger),
|
|
GamePhase::Construction(construction) => construction.update(input, sfx, loader),
|
|
GamePhase::Execute(execute) => execute.update(input, sfx, loader),
|
|
GamePhase::NextLevel => GamePhase::NextLevel,
|
|
GamePhase::Empty => panic!("bad state"),
|
|
}
|
|
}
|
|
|
|
fn render(&self, loader: &mut SpriteLoader, oam: &mut OamIterator) {
|
|
match self {
|
|
GamePhase::Empty => panic!("bad state"),
|
|
GamePhase::Lament(lament) => lament.render(oam),
|
|
GamePhase::Construction(construction) => construction.render(oam, loader),
|
|
GamePhase::Execute(execute) => execute.render(loader, oam),
|
|
GamePhase::NextLevel => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a, 'b> Game<'a, 'b> {
|
|
pub fn new(level: usize, background: &'a mut MapLoan<'b, RegularMap>) -> Self {
|
|
Self {
|
|
phase: GamePhase::Lament(Lament::new(level, background)),
|
|
}
|
|
}
|
|
|
|
pub fn update(
|
|
&mut self,
|
|
input: &ButtonController,
|
|
sfx: &mut Sfx,
|
|
loader: &mut SpriteLoader,
|
|
vram_manager: &mut VRamManager,
|
|
) -> bool {
|
|
self.phase.update(input, sfx, loader, vram_manager);
|
|
matches!(self.phase, GamePhase::NextLevel)
|
|
}
|
|
|
|
pub fn render(&self, loader: &mut SpriteLoader, oam: &mut OamIterator) {
|
|
self.phase.render(loader, oam)
|
|
}
|
|
|
|
pub fn set_background_visibility(&mut self, visible: bool) {
|
|
match &mut self.phase {
|
|
GamePhase::Construction(construction) => construction.background.set_visible(visible),
|
|
GamePhase::Execute(execute) => execute.construction.background.set_visible(visible),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Pausable<'a, 'b> {
|
|
paused: Paused,
|
|
menu: PauseMenu,
|
|
game: Game<'a, 'b>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
enum Paused {
|
|
Paused,
|
|
Playing,
|
|
}
|
|
|
|
impl Paused {
|
|
fn change(self) -> Paused {
|
|
match self {
|
|
Paused::Paused => Paused::Playing,
|
|
Paused::Playing => Paused::Paused,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub enum PauseSelection {
|
|
Restart,
|
|
LevelSelect(usize),
|
|
}
|
|
|
|
enum PauseSelectionInner {
|
|
Restart,
|
|
LevelSelect,
|
|
}
|
|
|
|
struct PauseMenu {
|
|
option_text: RefCell<[ObjectTextRender<'static>; 2]>,
|
|
selection: PauseSelectionInner,
|
|
indicator_sprite: SpriteVram,
|
|
selected_level: usize,
|
|
maximum_level: usize,
|
|
}
|
|
|
|
impl PauseMenu {
|
|
fn text_at_position(
|
|
text: core::fmt::Arguments,
|
|
position: Vector2D<i32>,
|
|
) -> ObjectTextRender<'static> {
|
|
let mut t = ObjectTextRender::new(&FONT, Size::S32x16, generate_text_palette());
|
|
|
|
let _ = writeln!(t, "{}", text);
|
|
t.layout(Vector2D::new(i32::MAX, i32::MAX), TextAlignment::Left, 0);
|
|
t.next_line();
|
|
t.update(position);
|
|
t
|
|
}
|
|
|
|
fn new(loader: &mut SpriteLoader, maximum_level: usize, current_level: usize) -> Self {
|
|
PauseMenu {
|
|
option_text: RefCell::new([
|
|
Self::text_at_position(format_args!("Restart"), Vector2D::new(32, HEIGHT / 4)),
|
|
Self::text_at_position(
|
|
format_args!("Go to level: {}", current_level + 1),
|
|
Vector2D::new(32, HEIGHT / 4 + 20),
|
|
),
|
|
]),
|
|
selection: PauseSelectionInner::Restart,
|
|
indicator_sprite: loader.get_vram_sprite(ARROW_RIGHT.sprite(0)),
|
|
selected_level: current_level,
|
|
maximum_level,
|
|
}
|
|
}
|
|
|
|
fn update(&mut self, input: &ButtonController) -> Option<PauseSelection> {
|
|
if input.is_just_pressed(Button::UP) | input.is_just_pressed(Button::DOWN) {
|
|
self.selection = match self.selection {
|
|
PauseSelectionInner::Restart => PauseSelectionInner::LevelSelect,
|
|
PauseSelectionInner::LevelSelect => PauseSelectionInner::Restart,
|
|
};
|
|
}
|
|
|
|
let lr = Tri::from((
|
|
input.is_just_pressed(Button::LEFT),
|
|
input.is_just_pressed(Button::RIGHT),
|
|
));
|
|
if matches!(self.selection, PauseSelectionInner::LevelSelect) && lr != Tri::Zero {
|
|
let selected_level = self.selected_level as i32;
|
|
let selected_level =
|
|
(selected_level + lr as i32).rem_euclid(self.maximum_level as i32 + 1);
|
|
self.selected_level = selected_level as usize;
|
|
self.option_text.borrow_mut()[1] = Self::text_at_position(
|
|
format_args!("Go to level: {}", selected_level + 1),
|
|
Vector2D::new(32, HEIGHT / 4 + 20),
|
|
)
|
|
}
|
|
|
|
if input.is_just_pressed(Button::A) | input.is_just_pressed(Button::START) {
|
|
Some(match self.selection {
|
|
PauseSelectionInner::Restart => PauseSelection::Restart,
|
|
PauseSelectionInner::LevelSelect => {
|
|
PauseSelection::LevelSelect(self.selected_level)
|
|
}
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn render(&self, oam: &mut OamIterator) {
|
|
for text in self.option_text.borrow_mut().iter_mut() {
|
|
text.commit(oam);
|
|
}
|
|
let mut indicator = ObjectUnmanaged::new(self.indicator_sprite.clone());
|
|
indicator.show();
|
|
match self.selection {
|
|
PauseSelectionInner::Restart => indicator.set_position(Vector2D::new(16, HEIGHT / 4)),
|
|
PauseSelectionInner::LevelSelect => {
|
|
indicator.set_position(Vector2D::new(16, HEIGHT / 4 + 20))
|
|
}
|
|
};
|
|
if let Some(slot) = oam.next() {
|
|
slot.set(&indicator);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub enum UpdateResult {
|
|
MenuSelection(PauseSelection),
|
|
NextLevel,
|
|
}
|
|
|
|
impl<'a, 'b> Pausable<'a, 'b> {
|
|
pub fn new(
|
|
level: usize,
|
|
maximum_level: usize,
|
|
background: &'a mut MapLoan<'b, RegularMap>,
|
|
loader: &mut SpriteLoader,
|
|
) -> Self {
|
|
Self {
|
|
paused: Paused::Playing,
|
|
game: Game::new(level, background),
|
|
menu: PauseMenu::new(loader, maximum_level, level),
|
|
}
|
|
}
|
|
|
|
pub fn update(
|
|
&mut self,
|
|
input: &ButtonController,
|
|
sfx: &mut Sfx,
|
|
loader: &mut SpriteLoader,
|
|
vram_manager: &mut VRamManager,
|
|
) -> Option<UpdateResult> {
|
|
if input.is_just_pressed(Button::SELECT)
|
|
|| (matches!(self.paused, Paused::Paused) && input.is_just_pressed(Button::B))
|
|
{
|
|
self.paused = self.paused.change();
|
|
match self.paused {
|
|
Paused::Paused => self.game.set_background_visibility(false),
|
|
Paused::Playing => self.game.set_background_visibility(true),
|
|
}
|
|
}
|
|
|
|
if !matches!(self.paused, Paused::Paused) {
|
|
if self.game.update(input, sfx, loader, vram_manager) {
|
|
Some(UpdateResult::NextLevel)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
self.menu.update(input).map(UpdateResult::MenuSelection)
|
|
}
|
|
}
|
|
|
|
pub fn render(&self, loader: &mut SpriteLoader, oam: &mut OamIterator) {
|
|
if matches!(self.paused, Paused::Paused) {
|
|
self.menu.render(oam);
|
|
} else {
|
|
self.game.render(loader, oam);
|
|
}
|
|
}
|
|
}
|