use agb::{ display::{ object::{OamIterator, ObjectUnmanaged, SpriteLoader, Tag}, tiled::{RegularMap, VRamManager}, }, fixnum::Vector2D, input::{Button, ButtonController, Tri}, }; use alloc::{vec, vec::Vec}; use crate::{ level::{Item, Level}, map::MapElement, resources, sfx::Sfx, }; use super::simulation::{Direction, Simulation}; pub const PLAY_AREA_WIDTH: usize = 11; pub const PLAY_AREA_HEIGHT: usize = 10; const ITEM_AREA_WIDTH: usize = 3; const ITEM_AREA_HEIGHT: usize = 3; const ITEM_AREA_TOP_LEFT: Vector2D = Vector2D::new(179, 96); const CURSOR_OFFSET: Vector2D = Vector2D::new(14, 14); const ARROW_TOP_LEFT: Vector2D = Vector2D::new(175, 15); pub struct GameState { level_number: usize, level: &'static Level, cursor_state: CursorState, frame: usize, item_states: Vec, } impl GameState { pub fn new(level_number: usize) -> Self { let level = Level::get_level(level_number); let position = level .entities .iter() .find(|x| x.0 == Item::Hero) .map(|hero| hero.1.x as usize + PLAY_AREA_WIDTH * hero.1.y as usize) .unwrap_or(PLAY_AREA_WIDTH * PLAY_AREA_HEIGHT / 2 + PLAY_AREA_WIDTH / 2); Self { level_number, level, cursor_state: CursorState { item_position: 0, board_position: position, current_place: CursorPlace::Item, held_item: None, }, frame: 0, item_states: vec![ItemState::default(); level.items.len()], } } pub fn create_simulation(&self, sfx: &mut Sfx, loader: &mut SpriteLoader) -> Simulation { Simulation::generate( self.item_states .iter() .zip(self.level.items) .filter_map(|(location, item)| match location { ItemState::Placed(loc) => Some((*loc, *item)), ItemState::NotPlaced => None, }) .map(|(location, item)| { ( item, Vector2D::new( (location % PLAY_AREA_WIDTH) as i32, (location / PLAY_AREA_WIDTH) as i32, ), ) }) .chain(self.level.entities.iter().map(|x| (x.0, x.1))), self.level, sfx, loader, ) } pub fn load_level_background(&self, map: &mut RegularMap, vram_manager: &mut VRamManager) { crate::backgrounds::load_level_background(map, vram_manager, self.level_number); } pub fn force_place(&mut self) { if self.cursor_state.current_place == CursorPlace::Board { let position_x = (self.cursor_state.board_position % PLAY_AREA_WIDTH) as i32; let position_y = (self.cursor_state.board_position / PLAY_AREA_WIDTH) as i32; let position: Vector2D<_> = (position_x, position_y).into(); let map_tile = self.level.map[(position_x, position_y)]; let fixed_item_at_location = self .level .entities .iter() .any(|entity| entity.1 == position); if map_tile == MapElement::Floor && !fixed_item_at_location { let placeable_item_at_location = self.item_states.iter().position(|state| match state { ItemState::Placed(position) => { *position == self.cursor_state.board_position } ItemState::NotPlaced => false, }); if placeable_item_at_location.is_none() { if let Some(held_item) = self.cursor_state.held_item { self.item_states[held_item] = ItemState::Placed(self.cursor_state.board_position); self.cursor_state.held_item = None; } } } } } pub fn step(&mut self, input: &ButtonController, sfx: &mut Sfx) { self.frame = self.frame.wrapping_add(1); self.cursor_state.update_position(input); if input.is_just_pressed(Button::A) { match self.cursor_state.current_place { CursorPlace::Board => { let position_x = (self.cursor_state.board_position % PLAY_AREA_WIDTH) as i32; let position_y = (self.cursor_state.board_position / PLAY_AREA_WIDTH) as i32; let position: Vector2D<_> = (position_x, position_y).into(); let map_tile = self.level.map[(position_x, position_y)]; let fixed_item_at_location = self .level .entities .iter() .any(|entity| entity.1 == position); if map_tile == MapElement::Floor && !fixed_item_at_location { let placeable_item_at_location = self.item_states.iter().position(|state| match state { ItemState::Placed(position) => { *position == self.cursor_state.board_position } ItemState::NotPlaced => false, }); let played_sound = if let Some(held_item) = self.cursor_state.held_item { self.item_states[held_item] = ItemState::Placed(self.cursor_state.board_position); self.cursor_state.held_item = None; sfx.place(); true } else { false }; if let Some(placeable_item_at_location) = placeable_item_at_location { self.cursor_state.held_item = Some(placeable_item_at_location); self.item_states[placeable_item_at_location] = ItemState::NotPlaced; if !played_sound { sfx.select(); } } } else { sfx.bad_selection(); } } CursorPlace::Item => { let item_position = self.cursor_state.item_position; if matches!( self.item_states.get(item_position), Some(ItemState::NotPlaced) ) { sfx.select(); self.cursor_state.current_place = CursorPlace::Board; self.cursor_state.held_item = Some(item_position); } else { sfx.bad_selection(); } } } } if input.is_just_pressed(Button::B) { match self.cursor_state.current_place { CursorPlace::Item => { self.cursor_state.current_place = CursorPlace::Board; self.cursor_state.held_item = None; } CursorPlace::Board => { self.cursor_state.current_place = CursorPlace::Item; self.cursor_state.held_item = None; } } } } pub fn render_arrows( &self, loader: &mut SpriteLoader, oam: &mut OamIterator, current_turn: Option, ) { let is_odd_frame = if current_turn.is_some() { true } else { let frame_index = self.frame / 32; frame_index % 2 == 1 }; for ((i, direction), slot) in self.level.directions.iter().enumerate().zip(oam) { let x = (i % 4) as i32; let y = (i / 4) as i32; let arrow_position = ARROW_TOP_LEFT + (x * 15, y * 15).into(); let arrow_position = if is_odd_frame { arrow_odd_frame_offset(*direction) } else { (0, 0).into() } + arrow_position; let sprite_idx = if Some(i) == current_turn { 1 } else { 0 }; let mut arrow_obj = ObjectUnmanaged::new( loader.get_vram_sprite(arrow_for_direction(*direction).sprite(sprite_idx)), ); arrow_obj.show().set_position(arrow_position); slot.set(&arrow_obj); } } pub fn render(&self, loader: &mut SpriteLoader, mut oam: &mut OamIterator) { let frame_index = self.frame / 32; let is_odd_frame = frame_index % 2 == 1; let mut cursor_obj = ObjectUnmanaged::new(loader.get_vram_sprite(resources::CURSOR.sprite(0))); cursor_obj .show() .set_position(self.cursor_state.get_position(is_odd_frame)); if let Some(slot) = oam.next() { slot.set(&cursor_obj); } let level = self.level; self.render_arrows(loader, oam, None); fn placed_position(position: usize, item: &Item) -> Vector2D { let position_x = (position % PLAY_AREA_WIDTH) as i32; let position_y = (position / PLAY_AREA_WIDTH) as i32; let position = Vector2D::new(position_x, position_y); position * 16 + item.map_entity_offset() } if let Some(held) = self.cursor_state.held_item { let item = &level.items[held]; let item_position = placed_position(self.cursor_state.board_position, item); let mut item_obj = ObjectUnmanaged::new( loader.get_vram_sprite(item.tag().animation_sprite(frame_index)), ); item_obj.show().set_position(item_position); if let Some(slot) = oam.next() { slot.set(&item_obj); } } for ((item_position, item), slot) in level .items .iter() .enumerate() .filter_map(|(i, item)| { let item_position = match self.item_states[i] { ItemState::Placed(position) => placed_position(position, item), ItemState::NotPlaced => { if self.cursor_state.held_item == Some(i) { return None; } else { let x = (i % ITEM_AREA_WIDTH) as i32; let y = (i / ITEM_AREA_WIDTH) as i32; ITEM_AREA_TOP_LEFT + (x * 16, y * 16).into() } } }; Some((item_position, item)) }) .zip(&mut oam) { let mut item_obj = ObjectUnmanaged::new( loader.get_vram_sprite(item.tag().animation_sprite(frame_index)), ); item_obj.show().set_position(item_position); slot.set(&item_obj); } for (entity, slot) in level.entities.iter().zip(&mut oam) { let entity_position = entity.1 * 16 + entity.0.map_entity_offset(); let mut entity_obj = ObjectUnmanaged::new(loader.get_vram_sprite(entity.0.shadow_tag().sprite(0))); entity_obj.show().set_position(entity_position); slot.set(&entity_obj); } } } struct CursorState { item_position: usize, board_position: usize, current_place: CursorPlace, held_item: Option, } #[derive(PartialEq, Eq, Clone, Copy)] enum CursorPlace { Item, Board, } impl CursorState { fn get_position(&self, is_odd_frame: bool) -> Vector2D { let odd_frame_offset = if is_odd_frame { Vector2D::new(1, 1) } else { Vector2D::new(0, 0) }; let place_position: Vector2D<_> = match self.current_place { CursorPlace::Board => { let current_x = (self.board_position % PLAY_AREA_WIDTH) as i32; let current_y = (self.board_position / PLAY_AREA_WIDTH) as i32; (current_x * 16, current_y * 16).into() } CursorPlace::Item => { let current_x = (self.item_position % ITEM_AREA_WIDTH) as i32; let current_y = (self.item_position / ITEM_AREA_WIDTH) as i32; ITEM_AREA_TOP_LEFT + (current_x * 16, current_y * 16).into() } }; place_position + CURSOR_OFFSET + odd_frame_offset } fn update_position(&mut self, input: &ButtonController) { let ud: Tri = ( input.is_just_pressed(Button::UP), input.is_just_pressed(Button::DOWN), ) .into(); let lr: Tri = ( input.is_just_pressed(Button::LEFT), input.is_just_pressed(Button::RIGHT), ) .into(); if ud == Tri::Zero && lr == Tri::Zero { return; } match self.current_place { CursorPlace::Board => { let current_x = self.board_position % PLAY_AREA_WIDTH; let current_y = self.board_position / PLAY_AREA_WIDTH; let mut new_x = current_x.saturating_add_signed(lr as isize).max(1); let new_y = current_y .saturating_add_signed(ud as isize) .max(1) .min(PLAY_AREA_HEIGHT - 2); if new_x == PLAY_AREA_WIDTH - 1 { new_x = new_x.min(PLAY_AREA_WIDTH - 2); if self.held_item.is_none() { self.current_place = CursorPlace::Item; self.item_position = new_y.saturating_sub(5).clamp(0, ITEM_AREA_HEIGHT - 1) * ITEM_AREA_WIDTH; } } self.board_position = new_x + new_y * PLAY_AREA_WIDTH; } CursorPlace::Item => { let current_x = self.item_position % ITEM_AREA_WIDTH; let current_y = self.item_position / ITEM_AREA_WIDTH; let mut new_x = current_x.wrapping_add_signed(lr as isize); let new_y = current_y .saturating_add_signed(ud as isize) .min(ITEM_AREA_HEIGHT - 1); if new_x == usize::MAX { new_x = 0; self.current_place = CursorPlace::Board; self.board_position = (new_y + 5) * PLAY_AREA_WIDTH + PLAY_AREA_WIDTH - 2; } else { new_x = new_x.min(ITEM_AREA_WIDTH - 1); } self.item_position = new_x + new_y * ITEM_AREA_WIDTH; } } } } const fn arrow_for_direction(direction: Direction) -> &'static Tag { match direction { Direction::Up => resources::ARROW_UP, Direction::Down => resources::ARROW_DOWN, Direction::Left => resources::ARROW_LEFT, Direction::Right => resources::ARROW_RIGHT, } } const fn arrow_odd_frame_offset(direction: Direction) -> Vector2D { match direction { Direction::Up => Vector2D::new(0, -1), Direction::Down => Vector2D::new(0, 1), Direction::Left => Vector2D::new(-1, 0), Direction::Right => Vector2D::new(1, 0), } } #[derive(Default, Clone)] enum ItemState { Placed(usize), #[default] NotPlaced, }