423 lines
12 KiB
Rust
423 lines
12 KiB
Rust
//! The game itself.
|
|||
|
|
//!
|
||
|
|
//! The game is written the way the original is: as straight-line code that
|
||
|
|
//! calls [`Game::wait_frame`] whenever it wants the next frame (to animate, to
|
||
|
|
//! wait for a button, ...). It runs on its own thread; `wait_frame` hands the
|
||
|
|
//! finished picture and sound to the host and blocks until the host sends the
|
||
|
|
//! next frame's buttons.
|
||
|
|
//!
|
||
|
|
//! Modules: `menus` (title, how to play, password, puzzle list), `story`
|
||
|
|
//! (story screens and the ending), `map` (the overworld walk between levels)
|
||
|
|
//! and `puzzle` (the puzzle itself and the level-clear screen).
|
||
|
|
|
||
|
|
mod map;
|
||
|
|
mod menus;
|
||
|
|
mod puzzle;
|
||
|
|
mod story;
|
||
|
|
|
||
|
|
use std::sync::mpsc::{Receiver, SyncSender};
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use crate::apu::{Apu, CLOCK_HZ};
|
||
|
|
use crate::input::Joypad;
|
||
|
|
use crate::rom::{GameData, Shape};
|
||
|
|
use crate::sound::SoundDriver;
|
||
|
|
use crate::video::{Display, Sprite, Video, MAP0, SCREEN_H, SCREEN_W};
|
||
|
|
|
||
|
|
/// Clock cycles per frame (the Game Boy runs at ~59.73 frames per second).
|
||
|
|
pub const FRAME_CYCLES: u64 = 70_224;
|
||
|
|
pub const FRAME_HZ: f64 = CLOCK_HZ as f64 / FRAME_CYCLES as f64;
|
||
|
|
|
||
|
|
/// The normal palette: shade n for color n, reversed (the game's art is
|
||
|
|
/// stored with 0 = black).
|
||
|
|
const PALETTE: u8 = 0x1B;
|
||
|
|
|
||
|
|
// ---- actor slots (multi-tile sprites, drawn every frame) ----
|
||
|
|
/// puzzle cursor / held piece; also the piece that drops on the map
|
||
|
|
const CURSOR: usize = 0;
|
||
|
|
/// menu arrow on the title screen and puzzle list
|
||
|
|
const MENU_CURSOR: usize = 1;
|
||
|
|
/// the hero on the map, the storyteller, the password cursor
|
||
|
|
const HERO: usize = 2;
|
||
|
|
/// "something here" marker on the map; the second figure in story scenes
|
||
|
|
const MARKER: usize = 3;
|
||
|
|
/// the bird that brings the password on the map
|
||
|
|
const MESSENGER: usize = 4;
|
||
|
|
const ACTOR_SLOTS: usize = 12;
|
||
|
|
|
||
|
|
/// A sprite made of one shape's cells, positioned at (y, x) in sprite
|
||
|
|
/// coordinates (screen + 16, screen + 8).
|
||
|
|
#[derive(Clone, Copy, Default)]
|
||
|
|
pub struct Actor {
|
||
|
|
pub visible: bool,
|
||
|
|
pub y: u8,
|
||
|
|
pub x: u8,
|
||
|
|
pub shape: u8,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Actor {
|
||
|
|
const fn at(y: u8, x: u8, shape: u8) -> Actor {
|
||
|
|
Actor { visible: true, y, x, shape }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Where a placed piece is and which board tiles it covers.
|
||
|
|
#[derive(Clone, Copy, Default)]
|
||
|
|
struct PieceSlot {
|
||
|
|
y: u8,
|
||
|
|
x: u8,
|
||
|
|
/// the board tiles under the piece's cells
|
||
|
|
under: [u8; 6],
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The puzzle board: which piece id covers each background map cell.
|
||
|
|
type Board = Box<[u8; 0x400]>;
|
||
|
|
|
||
|
|
/// Scratch state that the original keeps in $C000-$CFFF and wipes at the
|
||
|
|
/// start of each puzzle (and at the title screen).
|
||
|
|
struct Scratch {
|
||
|
|
actors: [Actor; ACTOR_SLOTS],
|
||
|
|
/// id of the piece in hand (type << 4 | orientation), 0 = none
|
||
|
|
held: u8,
|
||
|
|
/// one slot per piece type
|
||
|
|
slots: [PieceSlot; 16],
|
||
|
|
slots_backup: [PieceSlot; 16],
|
||
|
|
board: Board,
|
||
|
|
board_backup: Board,
|
||
|
|
bridge: map::Bridge,
|
||
|
|
marker_blink: u8,
|
||
|
|
story: u8,
|
||
|
|
piece_drop_done: bool,
|
||
|
|
timer: Timer,
|
||
|
|
messenger: map::Messenger,
|
||
|
|
/// how far the hero can walk on the map
|
||
|
|
walk_left: u8,
|
||
|
|
walk_right: u8,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for Scratch {
|
||
|
|
fn default() -> Self {
|
||
|
|
Scratch {
|
||
|
|
actors: Default::default(),
|
||
|
|
held: 0,
|
||
|
|
slots: Default::default(),
|
||
|
|
slots_backup: Default::default(),
|
||
|
|
board: Box::new([0; 0x400]),
|
||
|
|
board_backup: Box::new([0; 0x400]),
|
||
|
|
bridge: Default::default(),
|
||
|
|
marker_blink: 0,
|
||
|
|
story: 0,
|
||
|
|
piece_drop_done: false,
|
||
|
|
timer: Timer::default(),
|
||
|
|
messenger: Default::default(),
|
||
|
|
walk_left: 0,
|
||
|
|
walk_right: 0,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Clone, Copy, Default, PartialEq)]
|
||
|
|
enum TimerMode {
|
||
|
|
#[default]
|
||
|
|
Hidden,
|
||
|
|
Running,
|
||
|
|
Stopped,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The clear-time clock in the top right corner.
|
||
|
|
#[derive(Clone, Copy, Default)]
|
||
|
|
struct Timer {
|
||
|
|
mode: TimerMode,
|
||
|
|
frames: u8,
|
||
|
|
/// seconds, tens of seconds, minutes, tens of minutes, hours
|
||
|
|
digits: [u8; 5],
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Clone, Copy, Default)]
|
||
|
|
struct Palettes {
|
||
|
|
bg: u8,
|
||
|
|
obj0: u8,
|
||
|
|
obj1: u8,
|
||
|
|
/// palette 1 inverts every 16 frames (makes the cursor blink)
|
||
|
|
blink: u8,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// What the game hands the host each frame.
|
||
|
|
pub struct Frame {
|
||
|
|
/// one shade (0 = white .. 3 = black) per pixel
|
||
|
|
pub pixels: Vec<u8>,
|
||
|
|
/// interleaved stereo samples
|
||
|
|
pub audio: Vec<f32>,
|
||
|
|
/// a peek at the game state, for testing
|
||
|
|
pub probe: Probe,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Clone, Copy, Debug, Default)]
|
||
|
|
pub struct Probe {
|
||
|
|
pub level: u8,
|
||
|
|
pub cursor_y: u8,
|
||
|
|
pub cursor_x: u8,
|
||
|
|
pub held: u8,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The game thread's end of the connection to the host.
|
||
|
|
pub struct Link {
|
||
|
|
pub frames: SyncSender<Frame>,
|
||
|
|
pub buttons: Receiver<u8>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Unwinds the game thread when the host goes away.
|
||
|
|
struct Quit;
|
||
|
|
|
||
|
|
pub struct Game {
|
||
|
|
data: Arc<GameData>,
|
||
|
|
link: Link,
|
||
|
|
video: Video,
|
||
|
|
apu: Apu,
|
||
|
|
sound: SoundDriver,
|
||
|
|
joy: Joypad,
|
||
|
|
level: u8,
|
||
|
|
pieces_needed: u8,
|
||
|
|
palettes: Palettes,
|
||
|
|
/// sprites built this frame; the display picks them up at the next vblank
|
||
|
|
oam: [Sprite; 40],
|
||
|
|
s: Scratch,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Run the game on a new thread.
|
||
|
|
pub fn spawn(data: Arc<GameData>, sample_rate: u32, link: Link) -> std::thread::JoinHandle<()> {
|
||
|
|
std::thread::spawn(move || {
|
||
|
|
let mut game = Game::new(data, sample_rate, link);
|
||
|
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| game.run()));
|
||
|
|
if let Err(e) = result {
|
||
|
|
if !e.is::<Quit>() {
|
||
|
|
std::panic::resume_unwind(e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Game {
|
||
|
|
fn new(data: Arc<GameData>, sample_rate: u32, link: Link) -> Game {
|
||
|
|
Game {
|
||
|
|
sound: SoundDriver::new(data.clone()),
|
||
|
|
data,
|
||
|
|
link,
|
||
|
|
video: Video::new(),
|
||
|
|
apu: Apu::new(sample_rate),
|
||
|
|
joy: Joypad::default(),
|
||
|
|
level: 0,
|
||
|
|
pieces_needed: 0,
|
||
|
|
palettes: Palettes::default(),
|
||
|
|
oam: [Sprite::default(); 40],
|
||
|
|
s: Scratch::default(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run(&mut self) -> ! {
|
||
|
|
loop {
|
||
|
|
self.reset();
|
||
|
|
match self.title_screen() {
|
||
|
|
menus::TitleChoice::HowToPlay => {
|
||
|
|
self.how_to_play();
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
menus::TitleChoice::Password => {
|
||
|
|
self.password_screen();
|
||
|
|
self.puzzle_list();
|
||
|
|
}
|
||
|
|
menus::TitleChoice::Start => {}
|
||
|
|
}
|
||
|
|
self.play_levels();
|
||
|
|
self.ending();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Story, map, puzzle, repeat; returns after the last level.
|
||
|
|
fn play_levels(&mut self) {
|
||
|
|
loop {
|
||
|
|
self.s.timer.mode = TimerMode::Hidden;
|
||
|
|
self.story_screen();
|
||
|
|
self.story_text();
|
||
|
|
self.map_screen();
|
||
|
|
self.map_walk();
|
||
|
|
self.puzzle_screen();
|
||
|
|
self.s.timer.mode = TimerMode::Running;
|
||
|
|
loop {
|
||
|
|
self.wait_frame();
|
||
|
|
self.check_reset_puzzle();
|
||
|
|
self.move_cursor();
|
||
|
|
self.piece_action();
|
||
|
|
if self.pieces_in_place() == self.pieces_needed {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
self.s.timer.mode = TimerMode::Stopped;
|
||
|
|
self.level_clear();
|
||
|
|
if self.level as usize + 1 >= crate::rom::LEVEL_COUNT {
|
||
|
|
self.s.timer.mode = TimerMode::Hidden;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
self.level += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Back to power-on state (the title screen clears all of RAM).
|
||
|
|
fn reset(&mut self) {
|
||
|
|
self.video.display = Display::Off;
|
||
|
|
self.s = Scratch::default();
|
||
|
|
self.level = 0;
|
||
|
|
self.pieces_needed = 0;
|
||
|
|
self.palettes = Palettes::default();
|
||
|
|
self.joy = Joypad::default();
|
||
|
|
self.sound = SoundDriver::new(self.data.clone());
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------- frames
|
||
|
|
|
||
|
|
/// Finish this frame and wait for the next one.
|
||
|
|
fn wait_frame(&mut self) {
|
||
|
|
let mut pixels = vec![0; SCREEN_W * SCREEN_H];
|
||
|
|
self.video.render(&mut pixels);
|
||
|
|
let probe = Probe {
|
||
|
|
level: self.level,
|
||
|
|
cursor_y: self.s.actors[CURSOR].y,
|
||
|
|
cursor_x: self.s.actors[CURSOR].x,
|
||
|
|
held: self.s.held,
|
||
|
|
};
|
||
|
|
let frame = Frame { pixels, audio: std::mem::take(&mut self.apu.out), probe };
|
||
|
|
if self.link.frames.send(frame).is_err() {
|
||
|
|
std::panic::resume_unwind(Box::new(Quit));
|
||
|
|
}
|
||
|
|
let buttons = match self.link.buttons.recv() {
|
||
|
|
Ok(b) => b,
|
||
|
|
Err(_) => std::panic::resume_unwind(Box::new(Quit)),
|
||
|
|
};
|
||
|
|
|
||
|
|
// things the original does every vblank
|
||
|
|
self.video.sprites = self.oam;
|
||
|
|
self.update_palettes();
|
||
|
|
self.update_cursor_shape();
|
||
|
|
self.build_sprites();
|
||
|
|
self.joy.update(buttons);
|
||
|
|
self.update_bridge();
|
||
|
|
self.update_timer();
|
||
|
|
self.draw_timer();
|
||
|
|
self.update_marker_blink();
|
||
|
|
self.update_messenger();
|
||
|
|
self.sound.update(&mut self.apu);
|
||
|
|
self.apu.run(FRAME_CYCLES);
|
||
|
|
}
|
||
|
|
|
||
|
|
fn wait_frames(&mut self, n: u8) {
|
||
|
|
for _ in 0..n {
|
||
|
|
self.wait_frame();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn update_palettes(&mut self) {
|
||
|
|
let p = &mut self.palettes;
|
||
|
|
self.video.bg_palette = p.bg;
|
||
|
|
self.video.obj_palettes = [p.obj0, p.obj1];
|
||
|
|
let (blink, wrapped) = p.blink.overflowing_add(0x10);
|
||
|
|
p.blink = blink;
|
||
|
|
if wrapped {
|
||
|
|
p.obj1 ^= 0xFF;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn set_palettes(&mut self, v: u8) {
|
||
|
|
self.palettes.bg = v;
|
||
|
|
self.palettes.obj0 = v;
|
||
|
|
self.palettes.obj1 = v;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Turn the screen on showing the given background map.
|
||
|
|
fn show(&mut self, map: u16) {
|
||
|
|
self.video.display = Display::On(map);
|
||
|
|
}
|
||
|
|
|
||
|
|
fn play(&mut self, sound: u8) {
|
||
|
|
self.sound.play(sound);
|
||
|
|
}
|
||
|
|
|
||
|
|
fn shape(&self, n: u8) -> &Shape {
|
||
|
|
&self.data.shapes[n as usize]
|
||
|
|
}
|
||
|
|
|
||
|
|
fn clear_actors(&mut self) {
|
||
|
|
self.s.actors = Default::default();
|
||
|
|
self.s.held = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The cursor actor shows the held piece, or the cursor itself (which
|
||
|
|
/// changes shape over the piece tray on the right).
|
||
|
|
fn update_cursor_shape(&mut self) {
|
||
|
|
let a = &mut self.s.actors[CURSOR];
|
||
|
|
a.shape = if self.s.held != 0 {
|
||
|
|
self.data.piece_shape(self.s.held)
|
||
|
|
} else if a.x >= 0xA4 {
|
||
|
|
0x5D
|
||
|
|
} else {
|
||
|
|
0
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
fn build_sprites(&mut self) {
|
||
|
|
let mut sprites = [Sprite::default(); 40];
|
||
|
|
let mut n = 0;
|
||
|
|
for a in self.s.actors.iter().filter(|a| a.visible) {
|
||
|
|
let shape = &self.data.shapes[a.shape as usize];
|
||
|
|
for cell in &shape.cells {
|
||
|
|
let (dy, dx) = cell.sprite_offset();
|
||
|
|
if let Some(s) = sprites.get_mut(n) {
|
||
|
|
*s = Sprite { y: a.y.wrapping_add(dy), x: a.x.wrapping_add(dx), tile: cell.tile, attr: shape.attr };
|
||
|
|
}
|
||
|
|
n += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
self.oam = sprites;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ----------------------------------------------------------------- timer
|
||
|
|
|
||
|
|
fn update_timer(&mut self) {
|
||
|
|
let t = &mut self.s.timer;
|
||
|
|
if t.mode != TimerMode::Running {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
t.frames += 1;
|
||
|
|
if t.frames < 60 {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
t.frames = 0;
|
||
|
|
for (digit, limit) in t.digits.iter_mut().zip([10, 6, 10, 6, 10]) {
|
||
|
|
*digit += 1;
|
||
|
|
if *digit < limit {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
*digit = 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Two-tile-tall digits in the last 10 sprite slots: h mm ss.
|
||
|
|
fn draw_timer(&mut self) {
|
||
|
|
let t = self.s.timer;
|
||
|
|
if t.mode == TimerMode::Hidden {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
for (i, x) in [0x88, 0x80, 0x70, 0x68, 0x58].into_iter().enumerate() {
|
||
|
|
let tile = t.digits[i] * 2 + 0x64;
|
||
|
|
self.oam[30 + 2 * i] = Sprite { y: 0x10, x, tile, attr: 0 };
|
||
|
|
self.oam[31 + 2 * i] = Sprite { y: 0x18, x, tile: tile + 1, attr: 0 };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Map cell under a point in sprite coordinates.
|
||
|
|
fn cell_at(y: u8, x: u8) -> u16 {
|
||
|
|
let row = (y.wrapping_sub(16) >> 3) as u16;
|
||
|
|
let col = (x.wrapping_sub(8) >> 3) as u16;
|
||
|
|
MAP0 + row * 32 + col
|
||
|
|
}
|