commit a370ec7fd0d1a9429b2325e0965a73ee0556f576 Author: dani Date: Thu Jul 20 04:39:34 2023 +0000 all the things and even documentation... <_< diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/gsa.iml b/.idea/gsa.iml new file mode 100644 index 0000000..7025ac1 --- /dev/null +++ b/.idea/gsa.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..146ab09 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..45f7941 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..889b28c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ + +[package] +name = "gsa" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lazy_static = "1.4.0" +glam = "0.24.0" +ascii = "1.1.0" +gilrs = "0.10.2" + +[dependencies.skunk2d] +path = "../skunk2d" + +[profile.release-dani] +inherits = "release" +opt-level = "z" +lto = true +codegen-units = 1 +strip = true +panic = "abort" + diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..8e71710 --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,23 @@ +use glam::IVec2; +use gsa::{run, Gsa}; + +struct Game {} + +fn init(gsa: &mut Gsa) -> Game { + gsa.sprites[0].tile = 0x0300; + gsa.sprites[1].tile = 0x0200; + gsa.maps[0].tiles[0][0] = 0x0300; + gsa.maps[1].half_tile = true; + gsa.write_string(1, IVec2::ONE, "Hello world nyaa~"); + Game {} +} + +fn update(_game: &mut Game, gsa: &mut Gsa) { + gsa.sprites[0].pos.x = (gsa.sprites[0].pos.x + 1) % 300; + gsa.sprites[1].pos += gsa.input.dir; + if gsa.input.pressed.face_down { + gsa.sprites[1].tile += 1; + } +} + +run!(init, update); diff --git a/examples/gfx.gif b/examples/gfx.gif new file mode 100644 index 0000000..dbd91c9 Binary files /dev/null and b/examples/gfx.gif differ diff --git a/src/buttons.rs b/src/buttons.rs new file mode 100644 index 0000000..dd89038 --- /dev/null +++ b/src/buttons.rs @@ -0,0 +1,47 @@ +/// State of all GSA buttons +#[derive(Default, Copy, Clone)] +pub struct Buttons { + /// UP on dpad + pub dpad_up: bool, + /// DOWN on dpad + pub dpad_down: bool, + /// LEFT on dpad + pub dpad_left: bool, + /// RIGHT on dpad + pub dpad_right: bool, + /// X on nintendo, Y on microsoft, TRIANGLE on sony + pub face_up: bool, + /// B on nintendo, A on microsoft, CROSS on sony + pub face_down: bool, + /// Y on nintendo, X on microsoft, SQUARE on sony + pub face_left: bool, + /// A on nintendo, B on microsoft, CIRCLE on sony + pub face_right: bool, + /// left shoulder button + pub l: bool, + /// right shoulder button + pub r: bool, + /// start button + pub start: bool, + /// select button + pub select: bool, +} + +impl Buttons { + pub(crate) fn pressed(old: &Buttons, new: &Buttons) -> Buttons { + Buttons { + dpad_up: !old.dpad_up && new.dpad_up, + dpad_down: !old.dpad_down && new.dpad_down, + dpad_left: !old.dpad_left && new.dpad_left, + dpad_right: !old.dpad_right && new.dpad_right, + face_up: !old.face_up && new.face_up, + face_down: !old.face_down && new.face_down, + face_left: !old.face_left && new.face_left, + face_right: !old.face_right && new.face_right, + l: !old.l && new.l, + r: !old.r && new.r, + start: !old.start && new.start, + select: !old.select && new.select, + } + } +} diff --git a/src/gsa.rs b/src/gsa.rs new file mode 100644 index 0000000..afc895d --- /dev/null +++ b/src/gsa.rs @@ -0,0 +1,56 @@ +use crate::input::Input; +use crate::rgb::Rgb; +use crate::sprite::Sprite; +use crate::tilemap::Tilemap; +use crate::{MAX_SPRITES, MAX_TILEMAPS, TILEMAP_MAX_SIZE}; +use ascii::{AsciiChar, AsciiStr}; +use glam::IVec2; + +/// Complete state of GSA +pub struct Gsa { + /// Sprites available + pub sprites: [Sprite; MAX_SPRITES], + + /// Palette used to draw graphics, initially loaded from gfx.gif + pub palette: [Rgb; 256], + + /// Tilemap layers available + pub maps: [Tilemap; MAX_TILEMAPS], + + /// Currently selected font + /// + /// Chosen as half-size tile index, extends 16x16 half tiles in x and y + pub font: u16, + + /// Current input state + pub input: Input, +} + +impl Gsa { + /// Clears all tiles of given map to 0 + pub fn clear_map(&mut self, map: usize) { + self.fill_map(map, 0); + } + + /// Sets all tiles of map to val + pub fn fill_map(&mut self, map: usize, val: u16) { + for x in 0..TILEMAP_MAX_SIZE { + for y in 0..TILEMAP_MAX_SIZE { + self.maps[map].tiles[x][y] = val; + } + } + } + + /// Write given string on given map, at given position, with font [Gsa::font] + pub fn write_string(&mut self, map: usize, pos: IVec2, str: &str) { + let str = AsciiStr::from_ascii(str).unwrap(); + for (i, ch) in str.into_iter().enumerate() { + self.maps[map].tiles[pos.x as usize + i][pos.y as usize] = self.get_char_tile(*ch); + } + } + + fn get_char_tile(&self, ch: AsciiChar) -> u16 { + let ch = ch as u16; + self.font + (ch % 0x10) + (ch / 0x10) * 0x100 + } +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..7d722a8 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,18 @@ +use crate::buttons::Buttons; +use glam::IVec2; + +/// Input State +/// +/// GSA pretends all input is a snes-like gamepad +/// d-pad, 4 face buttons, l and r shoulder buttons, start and select +#[derive(Default)] +pub struct Input { + /// Buttons that are currentle held down + pub down: Buttons, + + /// Buttons that have been pressed in the current frame + pub pressed: Buttons, + + /// Directional vector (-1 to 1) from current dpad state + pub dir: IVec2, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..50aaf08 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,47 @@ +#![deny(missing_docs)] + +//! # Game Skunk Advance +//! Game development library modelled after an imaginary console +//! +//! ## Specs +//! - Resolution: 304x176 (19x11 tiles) +//! - Colors: 256 (indexed out of a possible 24-bit) +//! - Tilesize: 16x16 (or 8x8 for half-tiles) +//! - Tileset: 65536 tiles, indexed via 0xYYXX +//! - Sprites: 256 of size 16x16 (pondering allowing larger sprites) +//! - Tilemaps: 4 of size 1024x1024, scrollable +//! +//! ## Features not yet implemented +//! - Sound (no samples +//! - Synth +//! - Speech +//! - Savegames +//! - Helpers +//! - Gamepad text keyboard input +//! - Menus + +mod buttons; +mod gsa; +mod input; +mod rgb; +mod run; +mod sprite; +mod state; +mod tilemap; + +pub use crate::buttons::Buttons; +pub use crate::gsa::Gsa; +pub use crate::input::Input; +pub use crate::rgb::Rgb; +pub use crate::run::run; +pub use crate::sprite::Sprite; +pub use crate::tilemap::Tilemap; + +/// Amount of sprites in [Gsa::sprites] +pub const MAX_SPRITES: usize = 0xff; + +/// X and y dimensions of maps in [Gsa::maps] +pub const TILEMAP_MAX_SIZE: usize = 1024; + +/// Amount of tile maps in [Gsa::maps] +pub const MAX_TILEMAPS: usize = 4; diff --git a/src/rgb.rs b/src/rgb.rs new file mode 100644 index 0000000..6f540d0 --- /dev/null +++ b/src/rgb.rs @@ -0,0 +1,10 @@ +/// RGB Color +#[derive(Copy, Clone)] +pub struct Rgb { + /// Red component 0-255 + pub r: u8, + /// Green component 0-255 + pub g: u8, + /// Blue component 0-255 + pub b: u8, +} diff --git a/src/run.rs b/src/run.rs new file mode 100644 index 0000000..6c78c6b --- /dev/null +++ b/src/run.rs @@ -0,0 +1,57 @@ +use crate::state::State; +use crate::{Gsa, Rgb, Sprite, MAX_SPRITES}; +use gilrs::Gilrs; +use skunk2d::Image8; + +/// Creates main function, includes gfx.gif, and calls run +/// +/// Pass the following two functions to it: +/// - fn init(gsa: &mut Gsa) -> TGame +/// - fn update(game: &mut Game, gsa: &mut Gsa) +/// +/// TGame can be any type, usually a struct that contains your game's state +#[macro_export] +macro_rules! run { + ($init: ident, $update: ident) => { + fn main() { + run($init, $update, include_bytes!("gfx.gif")); + } + }; +} + +/// This is called by [run!] +pub fn run( + init_fn: fn(gsa: &mut Gsa) -> TGame, + update_fn: fn(game: &mut TGame, gsa: &mut Gsa), + image_data: &[u8], +) { + let tileset = Image8::load_data(image_data); + let pal = Image8::load_data_palette(image_data); + let mut palette = [Rgb { r: 0, g: 0, b: 0 }; 256]; + + for i in 0..pal.len() / 3 { + palette[i] = Rgb { + r: pal[i * 3 + 2], + g: pal[i * 3 + 1], + b: pal[i * 3], + }; + } + + let mut gsa = Gsa { + sprites: [Sprite::default(); MAX_SPRITES], + maps: Default::default(), + palette, + font: 0x1010, + input: Default::default(), + }; + let game = init_fn(&mut gsa); + let state = State:: { + gsa, + game, + tileset, + update_fn, + first: true, + gilrs: Gilrs::new().unwrap(), + }; + skunk2d::run_with::>(304, 176, 60, state); +} diff --git a/src/sprite.rs b/src/sprite.rs new file mode 100644 index 0000000..c5bb194 --- /dev/null +++ b/src/sprite.rs @@ -0,0 +1,17 @@ +use glam::IVec2; +use std::fmt::{Display, Formatter}; + +/// Sprite which will be displayed on screen, unless tile=0 +#[derive(Default, Copy, Clone)] +pub struct Sprite { + /// Position on screen + pub pos: IVec2, + /// Tile index + pub tile: u16, +} + +impl Display for Sprite { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "[pos: {}, tile: {}]", self.pos, self.tile) + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..5f75783 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,146 @@ +use crate::buttons::Buttons; +use crate::{Gsa, TILEMAP_MAX_SIZE}; +use gilrs::{Button, EventType, Gilrs}; +use glam::IVec2; +use skunk2d::{Event, IRect, Image8, WindowState}; +use std::rc::Rc; + +pub struct State { + pub(crate) gsa: Gsa, + pub(crate) game: TGame, + pub(crate) update_fn: fn(game: &mut TGame, gsa: &mut Gsa), + pub(crate) tileset: Rc, + pub(crate) first: bool, + pub(crate) gilrs: Gilrs, +} + +impl skunk2d::Game for State { + fn update(&mut self, window_state: &mut WindowState) { + if self.first { + window_state.toggle_fullscreen(); + self.first = false; + } + let mut new_buttons = self.gsa.input.down; + while let Some(event) = self.gilrs.next_event() { + match event { + gilrs::Event { + event: EventType::ButtonPressed(button, _), + .. + } => match button { + Button::South => new_buttons.face_down = true, + Button::East => new_buttons.face_left = true, + Button::North => new_buttons.face_up = true, + Button::West => new_buttons.face_right = true, + Button::LeftTrigger | Button::LeftTrigger2 => new_buttons.l = true, + Button::RightTrigger | Button::RightTrigger2 => new_buttons.r = true, + Button::Select => new_buttons.select = true, + Button::Start => new_buttons.start = true, + Button::DPadUp => new_buttons.dpad_up = true, + Button::DPadDown => new_buttons.dpad_down = true, + Button::DPadLeft => new_buttons.dpad_left = true, + Button::DPadRight => new_buttons.dpad_right = true, + _ => {} + }, + gilrs::Event { + event: EventType::ButtonReleased(button, _), + .. + } => match button { + Button::South => new_buttons.face_down = false, + Button::East => new_buttons.face_left = false, + Button::North => new_buttons.face_up = false, + Button::West => new_buttons.face_right = false, + Button::LeftTrigger | Button::LeftTrigger2 => new_buttons.l = false, + Button::RightTrigger | Button::RightTrigger2 => new_buttons.r = false, + Button::Select => new_buttons.select = false, + Button::Start => new_buttons.start = false, + Button::DPadUp => new_buttons.dpad_up = false, + Button::DPadDown => new_buttons.dpad_down = false, + Button::DPadLeft => new_buttons.dpad_left = false, + Button::DPadRight => new_buttons.dpad_right = false, + _ => {} + }, + _ => (), + } + } + self.gsa.input.pressed = Buttons::pressed(&self.gsa.input.down, &new_buttons); + self.gsa.input.down = new_buttons; + self.gsa.input.dir = IVec2 { + x: if self.gsa.input.down.dpad_left { -1 } else { 0 } + + if self.gsa.input.down.dpad_right { 1 } else { 0 }, + y: if self.gsa.input.down.dpad_up { -1 } else { 0 } + + if self.gsa.input.down.dpad_down { 1 } else { 0 }, + }; + + //todo: don't if not updated... how check? <_< + for i in 0..=255 { + window_state.set_palette( + i, + self.gsa.palette[i as usize].r, + self.gsa.palette[i as usize].g, + self.gsa.palette[i as usize].b, + ); + } + (self.update_fn)(&mut self.game, &mut self.gsa); + } + + fn on_event(&mut self, _window_state: &mut WindowState, _event: Event) {} + + fn draw(&self, target: &mut Image8) { + target.clear(); + + for map in &self.gsa.maps { + let tcmult = if map.half_tile { 2 } else { 1 }; + let tilesize = if map.half_tile { 8 } else { 16 }; + let mut startx = map.scroll.x / tilesize; + let mut starty = map.scroll.y / tilesize; + let endx = (TILEMAP_MAX_SIZE as i32).min(startx + 20 * tcmult); + let endy = (TILEMAP_MAX_SIZE as i32).min(starty + 12 * tcmult); + startx = 0.max(startx); + starty = 0.max(starty); + for x in startx..endx { + for y in starty..endy { + let tile = map.tiles[x as usize][y as usize]; + if tile > 0 { + let ty = tile / 0x100; + let tx = tile % 0x100; + target.draw_image_partial( + IVec2 { + x: x * tilesize - map.scroll.x, + y: y * tilesize - map.scroll.y, + }, + &self.tileset, + IRect { + pos: IVec2 { + x: tx as i32 * tilesize, + y: ty as i32 * tilesize, + }, + size: IVec2 { + x: tilesize, + y: tilesize, + }, + }, + ) + } + } + } + } + + for sprite in &self.gsa.sprites { + if sprite.tile > 0 { + let ty = sprite.tile / 0x100; + let tx = sprite.tile % 0x100; + target.draw_image_partial( + sprite.pos, + &self.tileset, + IRect { + pos: IVec2 { + x: tx as i32 * 16, + y: ty as i32 * 16, + }, + size: IVec2 { x: 16, y: 16 }, + }, + ); + } + } + } +} diff --git a/src/tilemap.rs b/src/tilemap.rs new file mode 100644 index 0000000..21152ef --- /dev/null +++ b/src/tilemap.rs @@ -0,0 +1,22 @@ +use crate::TILEMAP_MAX_SIZE; +use glam::IVec2; + +/// Tilemap which will be rendered on screen +pub struct Tilemap { + /// Tiles in idx, accessible via \[x\]\[y\], x and y 0..[TILEMAP_MAX_SIZE] + pub tiles: Box<[[u16; TILEMAP_MAX_SIZE]; TILEMAP_MAX_SIZE]>, + /// Camera scroll (negative draw offset) for rendering + pub scroll: IVec2, + /// Are tiles indices half-tile indices? + pub half_tile: bool, +} + +impl Default for Tilemap { + fn default() -> Self { + Self { + tiles: [[0u16; TILEMAP_MAX_SIZE]; TILEMAP_MAX_SIZE].into(), + scroll: IVec2::ZERO, + half_tile: false, + } + } +} diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..f50ca28 --- /dev/null +++ b/todo.md @@ -0,0 +1,12 @@ +# TODO + +- Input (mapped minimal controller) + - Gamepad doooone :) + - Keyboard still gotta +- Sound + - Something synth'y would be nice... + - Speech synthesis...? :) +- Savegames +- Helpers + - Menu + - Fake Keyboard Text Input \ No newline at end of file