all the things and even documentation... <_<
This commit is contained in:
commit
a370ec7fd0
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/Cargo.lock
|
|
@ -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
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="CPP_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/examples" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
|
@ -0,0 +1,10 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/gsa.iml" filepath="$PROJECT_DIR$/.idea/gsa.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
|
@ -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"
|
||||
|
|
@ -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);
|
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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;
|
|
@ -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,
|
||||
}
|
|
@ -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<TGame: 'static>(
|
||||
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::<TGame> {
|
||||
gsa,
|
||||
game,
|
||||
tileset,
|
||||
update_fn,
|
||||
first: true,
|
||||
gilrs: Gilrs::new().unwrap(),
|
||||
};
|
||||
skunk2d::run_with::<Image8, State<TGame>>(304, 176, 60, state);
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<TGame> {
|
||||
pub(crate) gsa: Gsa,
|
||||
pub(crate) game: TGame,
|
||||
pub(crate) update_fn: fn(game: &mut TGame, gsa: &mut Gsa),
|
||||
pub(crate) tileset: Rc<Image8>,
|
||||
pub(crate) first: bool,
|
||||
pub(crate) gilrs: Gilrs,
|
||||
}
|
||||
|
||||
impl<TGame> skunk2d::Game<Image8> for State<TGame> {
|
||||
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 },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue