Compare commits

..

32 Commits

Author SHA1 Message Date
fd0f6ddb21 messing around with egui editor prototype 2022-03-13 15:25:30 +00:00
75ca8c84ea a couple of attempts at players 2022-03-12 18:35:09 +00:00
2349d7365e misc 2022-03-12 18:34:30 +00:00
1f231a8434 todo stuff 2022-03-12 18:34:02 +00:00
ec06ab6585 copyright year 2022-03-12 18:17:36 +00:00
7dae8f065f tweak license 2022-03-12 18:17:11 +00:00
c3ee95e31a public mocks etc. 2022-03-12 18:14:55 +00:00
c962748797 pub, clone 2022-03-12 18:13:03 +00:00
4dd334d6a4 WIP editor attempts (broken...) 2021-11-14 18:08:14 +00:00
946f8b7826 misc stuff (add Scenes to Game?) 2021-11-14 18:07:57 +00:00
751e3bb5f0 todo notes 2021-11-14 18:06:37 +00:00
14ffe95f7d scene tile stubs 2021-11-14 18:05:29 +00:00
8ca5acc240 fix palette tests 2021-11-14 18:05:09 +00:00
955645415d add palette name and colour count for GIMP palette 2021-11-14 18:04:46 +00:00
c1be226956 get palette colours by index 2021-11-14 18:04:06 +00:00
dc8fd68e32 fix blueprint palette 2021-11-14 18:03:37 +00:00
4e69df12d1 rename currently-unused mocks 2021-11-14 18:03:01 +00:00
1532700ea5 Image to DynamicImage (and test) 2021-11-14 18:02:13 +00:00
82c0f78177 Into impls for Colour (are these necessary?) 2021-11-14 18:01:31 +00:00
f323e0e841 avatar with clear background, for testing 2021-11-14 18:00:07 +00:00
a5e5939fd5 non-resizeable window 2021-11-14 17:49:52 +00:00
68f5306283 size state changes 2021-11-14 17:49:30 +00:00
0e5b0c4566 todo 2021-05-19 20:44:07 +01:00
98288a4426 fix game runner and start migrating to actual game data; fix game data loading errors 2021-05-19 20:09:12 +01:00
d93f777b66 remove old Thing 2021-05-19 18:07:00 +01:00
1a7d7c268a rename 2021-05-19 17:52:12 +01:00
c9c0942daf "get by name, tag" functions; update todo 2021-05-19 17:51:38 +01:00
a14cab8f41 tag avatar as player 2021-05-19 12:00:48 +01:00
cc1015cc7a cat and teacup 2021-05-19 11:56:07 +01:00
b72e8f5ac8 tiles and entities 2021-05-19 11:42:49 +01:00
6637d87d45 add Entity 2021-05-19 11:07:49 +01:00
0abc64946e add wall property to tile; why did I make "avatar" a tile? smh 2021-05-19 10:26:37 +01:00
23 changed files with 737 additions and 162 deletions

View File

@@ -10,13 +10,15 @@ keywords = ["game-engine", "smallgames", "pixel-art", "tracker-music"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
eframe = "^0.12.0"
egui = "^0.12.0"
epi = "^0.12.0"
env_logger = "0.8.3" env_logger = "0.8.3"
hex = "^0.4.3" hex = "^0.4.3"
image = "0.23.14" image = "0.23.14"
log = "0.4.14" log = "0.4.14"
pixels = "0.3.0" pixels = "0.3.0"
rodio = "^0.11.0" rodio-xm = "^0.1.1"
rodio-xm = { git = "https://tinybird.dev/max/rodio-xm/", branch = "master" }
serde = "^1.0.114" serde = "^1.0.114"
serde_derive = "^1.0.114" serde_derive = "^1.0.114"
toml = "^0.5.6" toml = "^0.5.6"

View File

@@ -1,6 +1,6 @@
ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4) ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4)
Copyright © 2021 Max Bradbury Copyright © 2022 Max Bradbury
This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles. This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles.
@@ -11,8 +11,7 @@ Permission is hereby granted, free of charge, to any person or organization (the
2. The User is one of the following: 2. The User is one of the following:
a. An individual person, laboring for themselves a. An individual person, laboring for themselves
b. A non-profit organization b. A non-profit organization
c. An educational institution c. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labour
d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor
3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote. 3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote.

47
TODO.md
View File

@@ -2,6 +2,22 @@
## game data structure ## game data structure
more colours? max 10? (0-9) max 16? (0-f)
colours not in palette can use modulo
background palette and foreground palette? would be good to make interactive stuff obvious
--
add "move" functions for entities
e.g. move(direction), teleport
--
add "from_dir" functions for structs
make that function generic?
--
what is the distinction between animate and inanimate objects? what is the distinction between animate and inanimate objects?
idea: idea:
@@ -12,21 +28,7 @@ idea:
* each position could have (optionally) a tile and (optionally) a thing * each position could have (optionally) a tile and (optionally) a thing
* things have render priority * things have render priority
### colours * ideally support gif/png for images (are there other indexed colour formats?)
~convert colours to hex~
* ~accept abcdef, #abcdef, 0xabcdef (inc. uppercase)~
* ~export as #abcdef~
### palettes
### images
* ideally support gif/png...
### parser
* ~move tests into their respective modules where appropriate~
## players ## players
@@ -34,10 +36,11 @@ idea:
* re-use player avatar drawing function as generic image drawing function * re-use player avatar drawing function as generic image drawing function
* text (how?) * text (how?)
* support older graphics adaptors * support older graphics adaptors
* player has some graphical tearing on non-integer window sizes
### linux
* ~get working~ * show a help splash screen on first boot (with "do not show me this again" checkbox which saves to a local config)
* what is peachy?
* controls
### windows ### windows
@@ -62,3 +65,11 @@ will need:
* build something in egui * build something in egui
* can we do a web version that works with zip files? * can we do a web version that works with zip files?
* investigate zip compression/decompression in rust * investigate zip compression/decompression in rust
## documentation
where to find XMs?
https://modarchive.org/index.php?request=view_by_license&query=publicdomain etc.
keygenmusic.org?
pouet.net or something?

120
src/bin/editor.rs Normal file
View File

@@ -0,0 +1,120 @@
use eframe::epi::Frame;
use eframe::egui::CtxRef;
use peachy::{Colour, Config, Game, Palette};
struct EditorState {
game: Option<peachy::Game>,
}
impl epi::App for EditorState {
fn update(&mut self, ctx: &CtxRef, frame: &mut Frame<'_>) {
egui::TopPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
egui::menu::menu(ui, "File", |ui| {
if ui.button("Quit").clicked() {
frame.quit();
}
});
});
});
egui::SidePanel::left("side_panel", 200.0).show(ctx, |ui| {
ui.heading("Side Panel");
ui.horizontal(|ui| {
ui.label("Blah blah…");
});
// if let Some(game) = &mut self.game {
// // todo colour picker
// // todo tool picker
// // todo collapsing panels for different things? buttons to launch browse windows?
// }
});
egui::CentralPanel::default().show(ctx, |ui| {
// todo if game is none, add a "load project" window?
if let Some(game) = &mut self.game {
egui::Window::new("palettes")
.default_width(200.0)
.collapsible(false)
.show(ctx, |ui| {
for palette in game.palettes.iter_mut() {
ui.columns(2, |columns | {
columns[0].label(&palette.name);
if columns[1].button("edit").clicked() {
egui::Window::new(&palette.name)
.default_width(200.0)
.collapsible(true)
.show(ctx, |ui| {
for (i, colour) in palette.colours.iter().enumerate() {
ui.columns(2, |row| {
// todo edit each colour... this is broken
row[0].color_edit_button_rgb(&mut [
colour.red as f32,
colour.green as f32,
colour.blue as f32
]);
if row[1].button("delete").clicked() {
// can't alter palettes while iterating... what to do?
// palette.colours.remove(i);
}
});
}
// todo "add colour" button
});
}
});
}
});
}
});
}
fn name(&self) -> &str {
"peachy"
}
}
fn main() {
// testing…
let state = EditorState {
game: Some(Game {
config: Config {
name: Some("example".into()),
width: 16,
height: 9,
tick: 400,
starting_room: None,
version: (0, 1)
},
entities: vec![],
images: vec![],
palettes: vec![
Palette { name: "example palette".into(), colours: vec![
Colour { red: 79, green: 30, blue: 69 },
Colour { red: 150, green: 48, blue: 87 },
Colour { red: 215, green: 68, blue: 89 },
Colour { red: 235, green: 112, blue: 96 },
Colour { red: 255, green: 179, blue: 131 },
Colour { red: 255, green: 255, blue: 255 },
Colour { red: 127, green: 227, blue: 187 },
Colour { red: 92, green: 187, blue: 196 },
Colour { red: 69, green: 126, blue: 163 },
Colour { red: 56, green: 66, blue: 118 },
Colour { red: 50, green: 36, blue: 81 }
]},
],
scenes: vec![],
tiles: vec![],
music: vec![]
}),
};
let native_options = eframe::NativeOptions::default();
eframe::run_native(Box::new(state), native_options);
}

View File

@@ -1,22 +1,25 @@
#[windows_subsystem = "windows"] #[windows_subsystem = "windows"]
use std::collections::HashMap;
use std::path::PathBuf;
use log::error; use log::error;
use pixels::{Error, SurfaceTexture, PixelsBuilder}; use pixels::{Error, SurfaceTexture, PixelsBuilder};
use pixels::wgpu::BackendBit; use pixels::wgpu::BackendBit;
use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize}; use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize, Size};
use winit::event::{Event, VirtualKeyCode}; use winit::event::{Event, VirtualKeyCode};
use winit::event_loop::{ControlFlow, EventLoop}; use winit::event_loop::{ControlFlow, EventLoop};
use winit_input_helper::WinitInputHelper; use winit_input_helper::WinitInputHelper;
use std::collections::HashMap;
use peachy::Game;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct Image { struct Image {
pixels: [u8; 64] pixels: [u8; 64]
} }
struct Game { struct State {
width: usize, game: Game,
height: usize,
player_position: (u8, u8), player_position: (u8, u8),
player_avatar: Image, player_avatar: Image,
palette: [[u8; 4]; 4], palette: [[u8; 4]; 4],
@@ -24,7 +27,7 @@ struct Game {
music: HashMap<String, rodio::Sink>, music: HashMap<String, rodio::Sink>,
} }
impl Game { impl State {
fn draw(&self, screen: &mut [u8]) { fn draw(&self, screen: &mut [u8]) {
// clear screen // clear screen
for pixel in screen.chunks_exact_mut(4) { for pixel in screen.chunks_exact_mut(4) {
@@ -42,13 +45,13 @@ impl Game {
screen[ screen[
( (
// player vertical offset (number of lines above) // player vertical offset (number of lines above)
(player_y as usize * 8 * (8 * self.width as usize)) (player_y as usize * 8 * (8 * self.game.config.width as usize))
+ +
// player horizontal offset; number of pixels to the left // player horizontal offset; number of pixels to the left
(player_x as usize * 8) (player_x as usize * 8)
+ +
// tile vertical offset; number of lines within tile // tile vertical offset; number of lines within tile
(tile_y as usize * (8 * self.width as usize)) (tile_y as usize * (8 * self.game.config.width as usize))
+ +
(tile_x as usize) // tile horizontal offset (tile_x as usize) // tile horizontal offset
) )
@@ -68,9 +71,11 @@ impl Game {
fn main() -> Result<(), Error> { fn main() -> Result<(), Error> {
env_logger::init(); env_logger::init();
let mut game = Game { let path = PathBuf::from("src/test-resources/basic");
width: 16, let game = peachy::Game::from_dir(path).unwrap();
height: 9,
let mut state = State {
game,
player_position: (8, 4), player_position: (8, 4),
player_avatar: Image { pixels: [ player_avatar: Image { pixels: [
0,0,0,2,2,0,0,0, 0,0,0,2,2,0,0,0,
@@ -97,48 +102,50 @@ fn main() -> Result<(), Error> {
let (window, p_width, p_height, mut _hidpi_factor) = create_window( let (window, p_width, p_height, mut _hidpi_factor) = create_window(
"pixels test", "pixels test",
(game.width * 8) as f64, (state.game.config.width * 8) as f64,
(game.height * 8) as f64, (state.game.config.height * 8) as f64,
&event_loop &event_loop
); );
let surface_texture = SurfaceTexture::new(p_width, p_height, &window); let surface_texture = SurfaceTexture::new(p_width, p_height, &window);
let mut pixels = PixelsBuilder::new( let mut pixels = PixelsBuilder::new(
(game.width * 8) as u32, (game.height * 8) as u32, surface_texture (state.game.config.width * 8) as u32,
(state.game.config.height * 8) as u32,
surface_texture
) )
.wgpu_backend(BackendBit::GL | BackendBit::PRIMARY) .wgpu_backend(BackendBit::GL | BackendBit::PRIMARY)
.enable_vsync(false) .enable_vsync(false)
.build()?; .build()?;
let device = rodio::default_output_device().unwrap(); // let device = rodio::default_output_device().unwrap();
//
// let source = rodio_xm::XMSource::from_bytes(
// include_bytes!("../ninety degrees.xm")
// );
//
// let sink = rodio::Sink::new(&device);
// sink.append(source);
// sink.pause();
//
// game.music.insert(":ninety degrees".into(), sink);
//
// let source = rodio_xm::XMSource::from_bytes(
// include_bytes!("../orn_keygentheme2001.xm")
// );
//
// let sink = rodio::Sink::new(&device);
// sink.append(source);
// sink.pause();
//
// game.music.insert("orn_keygentheme2001".into(), sink);
let source = rodio_xm::XMSource::from_bytes( state.current_music = None;
include_bytes!("../ninety degrees.xm")
);
let sink = rodio::Sink::new(&device);
sink.append(source);
sink.pause();
game.music.insert(":ninety degrees".into(), sink);
let source = rodio_xm::XMSource::from_bytes(
include_bytes!("../orn_keygentheme2001.xm")
);
let sink = rodio::Sink::new(&device);
sink.append(source);
sink.pause();
game.music.insert("orn_keygentheme2001".into(), sink);
game.current_music = None;
event_loop.run(move |event, _, control_flow| { event_loop.run(move |event, _, control_flow| {
// The one and only event that winit_input_helper doesn't have for us... // The one and only event that winit_input_helper doesn't have for us...
if let Event::RedrawRequested(_) = event { if let Event::RedrawRequested(_) = event {
game.draw(pixels.get_frame()); state.draw(pixels.get_frame());
if pixels if pixels
.render() .render()
@@ -161,49 +168,65 @@ fn main() -> Result<(), Error> {
if input.key_pressed(VirtualKeyCode::M) { if input.key_pressed(VirtualKeyCode::M) {
// pause the current tune // pause the current tune
if game.current_music.is_some() { if state.current_music.is_some() {
game.music.get(game.current_music.as_ref().unwrap()).unwrap().pause(); state.music.get(state.current_music.as_ref().unwrap()).unwrap().pause();
} }
if game.current_music.is_none() || game.current_music.as_ref().unwrap() == "orn_keygentheme2001" { if state.current_music.is_none() || state.current_music.as_ref().unwrap() == "orn_keygentheme2001" {
// play the first tune // play the first tune
game.current_music = Some(":ninety degrees".into()); state.current_music = Some(":ninety degrees".into());
game.music.get(game.current_music.as_ref().unwrap()).unwrap().play(); state.music.get(state.current_music.as_ref().unwrap()).unwrap().play();
} else { } else {
// play the second tune // play the second tune
game.current_music = Some("orn_keygentheme2001".into()); state.current_music = Some("orn_keygentheme2001".into());
game.music.get(game.current_music.as_ref().unwrap()).unwrap().play(); state.music.get(state.current_music.as_ref().unwrap()).unwrap().play();
} }
} }
if input.key_pressed(VirtualKeyCode::Left) { if
let (x, y) = game.player_position; input.key_pressed(VirtualKeyCode::Left)
||
input.key_pressed(VirtualKeyCode::W)
{
let (x, y) = state.player_position;
if x > 0 { if x > 0 {
game.player_position = (x - 1, y); state.player_position = (x - 1, y);
window.request_redraw(); window.request_redraw();
} }
} }
if input.key_pressed(VirtualKeyCode::Right) { if
let (x, y) = game.player_position; input.key_pressed(VirtualKeyCode::Right)
if x < game.width as u8 - 1 { ||
game.player_position = (x + 1, y); input.key_pressed(VirtualKeyCode::D)
{
let (x, y) = state.player_position;
if x < state.game.config.width as u8 - 1 {
state.player_position = (x + 1, y);
window.request_redraw(); window.request_redraw();
} }
} }
if input.key_pressed(VirtualKeyCode::Up) { if
let (x, y) = game.player_position; input.key_pressed(VirtualKeyCode::Up)
||
input.key_pressed(VirtualKeyCode::W)
{
let (x, y) = state.player_position;
if y > 0 { if y > 0 {
game.player_position = (x, y - 1); state.player_position = (x, y - 1);
window.request_redraw(); window.request_redraw();
} }
} }
if input.key_pressed(VirtualKeyCode::Down) { if
let (x, y) = game.player_position; input.key_pressed(VirtualKeyCode::Down)
if y < game.height as u8 - 1 { ||
game.player_position = (x, y + 1); input.key_pressed(VirtualKeyCode::S)
{
let (x, y) = state.player_position;
if y < state.game.config.height as u8 - 1 {
state.player_position = (x, y + 1);
window.request_redraw(); window.request_redraw();
} }
} }
@@ -229,6 +252,7 @@ fn main() -> Result<(), Error> {
fn window_builder(title: &str, event_loop: &EventLoop<()>) -> winit::window::Window { fn window_builder(title: &str, event_loop: &EventLoop<()>) -> winit::window::Window {
winit::window::WindowBuilder::new() winit::window::WindowBuilder::new()
.with_visible(false) .with_visible(false)
// .with_resizable(false)
.with_title(title).build(&event_loop).unwrap() .with_title(title).build(&event_loop).unwrap()
} }
@@ -239,6 +263,7 @@ fn window_builder(title: &str, event_loop: &EventLoop<()>) -> winit::window::Win
winit::window::WindowBuilder::new() winit::window::WindowBuilder::new()
.with_drag_and_drop(false) .with_drag_and_drop(false)
.with_visible(false) .with_visible(false)
// .with_resizable(false)
.with_title(title).build(&event_loop).unwrap() .with_title(title).build(&event_loop).unwrap()
} }
@@ -273,21 +298,21 @@ fn create_window(
let scale = (monitor_height / height * 2.0 / 3.0).round().max(1.0); let scale = (monitor_height / height * 2.0 / 3.0).round().max(1.0);
// Resize, center, and display the window // Resize, centre, and display the window
let min_size: winit::dpi::LogicalSize<f64> = // let min_size: winit::dpi::LogicalSize<f64> =
PhysicalSize::new(width, height).to_logical(hidpi_factor); // PhysicalSize::new(width, height).to_logical(hidpi_factor);
let default_size = let default_size =
LogicalSize::new(width * scale, height * scale); LogicalSize::new(width * scale, height * scale);
let center = LogicalPosition::new( let centre = LogicalPosition::new(
(monitor_width - width * scale) / 2.0, (monitor_width - width * scale) / 2.0,
(monitor_height - height * scale) / 2.0, (monitor_height - height * scale) / 2.0,
); );
window.set_inner_size(default_size); window.set_inner_size(default_size);
window.set_min_inner_size(Some(min_size)); // window.set_min_inner_size(Some(min_size));
window.set_outer_position(center); window.set_outer_position(centre);
window.set_visible(true); window.set_visible(true);
let size = default_size.to_physical::<f64>(hidpi_factor); let size = default_size.to_physical::<f64>(hidpi_factor);

88
src/bin/player2.rs Normal file
View File

@@ -0,0 +1,88 @@
use raylib::prelude::*;
use raylib::consts::KeyboardKey;
use peachy::Colour;
// todo state
fn main() {
// todo load game
let game = peachy::mock::game::bitsy();
let (mut rl, thread) = raylib::init()
.size((game.config.width * 4) as i32, (game.config.height * 4) as i32)
.title("peachy")
.build();
rl.set_target_fps(30); // appropriate?
let key_up: KeyboardKey = raylib::core::input::key_from_i32(87).unwrap();
let key_left: KeyboardKey = raylib::core::input::key_from_i32(65).unwrap();
let key_down: KeyboardKey = raylib::core::input::key_from_i32(83).unwrap();
let key_right: KeyboardKey = raylib::core::input::key_from_i32(68).unwrap();
const SIZE: i32 = 32;
let mut x = SIZE;
let mut y = SIZE;
let palette = game.palettes.get(0).unwrap();
let avatars = game.get_entities_by_tag(&"avatar".to_string());
let avatar = avatars.get(0).unwrap();
let image = game.get_image_by_name(&avatar.image).unwrap().clone().into_image(palette);
// todo how do I create a texture without a file?
let mut texture = rl.load_texture(
&thread, "src/test-resources/images/avatar.png"
).unwrap();
texture.update_texture(image.as_bytes());
// let font = rl.load_font(&thread, "src/FuturaStd-Light.otf").unwrap();
let mut audio = audio::RaylibAudio::init_audio_device();
let mut music = raylib::audio::Music::load_music_stream(
&thread, "src/test-resources/music/another-night.xm"
).unwrap();
audio.play_music_stream(&mut music);
println!("{}", rl.window_should_close());
while !rl.window_should_close() {
if rl.is_key_pressed(key_up) {
y -= SIZE;
} else if rl.is_key_pressed(key_left) {
x -= SIZE;
} else if rl.is_key_pressed(key_down) {
y += SIZE;
} else if rl.is_key_pressed(key_right) {
x += SIZE;
}
audio.update_music_stream(&mut music);
let mut d = rl.begin_drawing(&thread);
// d.clear_background(palette.get_colour_raylib(&0));
d.clear_background(Color::WHITE);
d.draw_texture_ex(
&texture,
raylib::core::math::Vector2 { x: x as f32, y: y as f32 },
0.0,
4.0,
Color::WHITE
);
// d.draw_text_ex(
// &font,
// "hello",
// raylib::core::math::Vector2 { x: 64.0, y: 64.0 },
// 32.0,
// 4.0,
// Color::WHITE
// );
}
}

View File

@@ -1,5 +1,7 @@
use std::fmt; use std::fmt;
use image;
use image::Rgba;
use serde_derive::{Serialize, Deserialize}; use serde_derive::{Serialize, Deserialize};
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
@@ -37,6 +39,18 @@ impl Into<Vec<u8>> for Colour {
} }
} }
impl Into<image::Rgba<u8>> for Colour {
fn into(self) -> Rgba<u8> {
Rgba::from([self.red, self.green, self.blue, 255])
}
}
impl Into<[u8; 4]> for Colour {
fn into(self) -> [u8; 4] {
[self.red, self.green, self.blue, 255]
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::Colour; use crate::Colour;

View File

@@ -3,15 +3,15 @@ use serde_derive::{Serialize, Deserialize};
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Config { pub struct Config {
/// used in the window title bar /// used in the window title bar
name: Option<String>, pub name: Option<String>,
width: u8, pub width: u8,
height: u8, pub height: u8,
/// animation rate in milliseconds /// animation rate in milliseconds
tick: u64, pub tick: u64,
/// if this is not specified, the game will pick the first room it finds /// if this is not specified, the game will pick the first room it finds
starting_room: Option<String>, pub starting_room: Option<String>,
/// major / minor /// major / minor
version: (u8, u8), pub version: (u8, u8),
} }
#[cfg(test)] #[cfg(test)]

42
src/entity.rs Normal file
View File

@@ -0,0 +1,42 @@
use std::fs::read_to_string;
use std::path::PathBuf;
use serde_derive::{Serialize, Deserialize};
#[derive(Debug, Eq, PartialEq)]
pub struct Entity {
pub name: String,
pub image: String,
pub tags: Vec<String>,
}
impl Entity {
pub fn from_file(path: PathBuf) -> Self {
let name = path.file_stem().unwrap().to_str().unwrap().into();
let intermediate: IntermediateEntity = toml::from_str(
&read_to_string(path).unwrap()
).unwrap();
Self { name, image: intermediate.image, tags: intermediate.tags }
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct IntermediateEntity {
image: String,
tags: Vec<String>,
}
#[cfg(test)]
mod test {
use std::path::PathBuf;
use crate::entity::Entity;
#[test]
fn from_file() {
let path = PathBuf::from("src/test-resources/basic/entities/avatar.toml");
let output = Entity::from_file(path);
let expected = Entity { name: "avatar".into(), image: "avatar".into(), tags: vec![] };
assert_eq!(output, expected);
}
}

View File

@@ -1,8 +1,10 @@
use std::path::PathBuf; use std::path::PathBuf;
use serde_derive::{Serialize, Deserialize}; use serde_derive::{Serialize, Deserialize};
use std::fs::read_to_string; use std::fs::read_to_string;
use image::{DynamicImage, ImageBuffer};
use crate::Palette;
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Image { pub struct Image {
pub name: String, pub name: String,
/// colour indices - todo convert to [u8; 64]? /// colour indices - todo convert to [u8; 64]?
@@ -21,6 +23,16 @@ impl Image {
Self { name, pixels } Self { name, pixels }
} }
pub fn into_image(self, palette: &Palette) -> DynamicImage {
let mut buffer: Vec<u8> = Vec::new();
for pixel in self.pixels {
buffer.append(&mut palette.get_colour_rgba8(&pixel));
}
DynamicImage::ImageRgba8(ImageBuffer::from_raw(8, 8, buffer).unwrap())
}
} }
#[cfg(test)] #[cfg(test)]
@@ -35,4 +47,16 @@ mod test {
let expected = crate::mock::image::avatar(); let expected = crate::mock::image::avatar();
assert_eq!(output, expected); assert_eq!(output, expected);
} }
#[test]
fn image_to_dynamic_image() {
let palette = &crate::mock::palette::default();
let output = crate::mock::image::avatar().into_image(palette);
let expected = image::io::Reader::open(
"src/test-resources/images/avatar.png"
).unwrap().decode().unwrap();
assert_eq!(output, expected);
}
} }

View File

@@ -3,8 +3,9 @@ use std::path::PathBuf;
mod colour; mod colour;
mod config; mod config;
mod entity;
mod image; mod image;
mod mock; pub mod mock;
mod music; mod music;
mod palette; mod palette;
mod scene; mod scene;
@@ -12,10 +13,12 @@ mod tile;
pub use colour::Colour; pub use colour::Colour;
pub use config::Config; pub use config::Config;
pub use entity::Entity;
pub use crate::image::Image; pub use crate::image::Image;
pub use music::Music; pub use music::Music;
pub use palette::Palette; pub use palette::Palette;
pub use scene::Scene; pub use scene::Scene;
pub use tile::Tile;
#[derive(Debug, Eq, Hash, PartialEq)] #[derive(Debug, Eq, Hash, PartialEq)]
pub struct Position { pub struct Position {
@@ -23,13 +26,11 @@ pub struct Position {
y: u8, y: u8,
} }
// #[derive(Serialize, Deserialize)] impl Position {
// pub struct Thing { pub fn to_index(&self, width: u8) -> u16 {
// name: Option<String>, (self.y * width + self.x) as u16
// /// image name }
// image: String, }
// }
//
// #[derive(Serialize, Deserialize)] // #[derive(Serialize, Deserialize)]
// pub enum DataType { // pub enum DataType {
@@ -104,26 +105,47 @@ pub struct Position {
// } // }
pub struct Game { pub struct Game {
config: Config, pub config: Config,
palettes: Vec<Palette>, pub entities: Vec<Entity>,
images: Vec<Image>, pub images: Vec<Image>,
// todo tiles pub palettes: Vec<Palette>,
// todo things pub scenes: Vec<Scene>,
// variables: Vec<Variable>, pub tiles: Vec<Tile>,
// triggers: HashMap<String, ScriptCollection>, // pub variables: Vec<Variable>,
music: Vec<Music>, // pub triggers: HashMap<String, ScriptCollection>,
pub music: Vec<Music>,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct GameParseError; pub struct GameParseError;
impl Game { impl Game {
pub fn from(path: String) -> Result<Game, GameParseError> { pub fn new() -> Self {
let path = PathBuf::from(path); Self {
config: Config {
name: None,
width: 16,
height: 9,
tick: 400,
starting_room: None,
version: (0, 1)
},
entities: vec![],
images: vec![],
palettes: vec![],
scenes: vec![],
tiles: vec![],
music: vec![]
}
}
pub fn from_dir(path: PathBuf) -> Result<Game, GameParseError> {
let mut images = Vec::new(); let mut images = Vec::new();
let mut tiles = Vec::new();
let mut entities = Vec::new();
let mut music = Vec::new(); let mut music = Vec::new();
let mut palettes = Vec::new(); let mut palettes = Vec::new();
let mut scenes = Vec::new();
let mut music_dir = path.clone(); let mut music_dir = path.clone();
music_dir.push("music"); music_dir.push("music");
@@ -134,7 +156,6 @@ impl Game {
for file in music_files.unwrap() { for file in music_files.unwrap() {
let file = file.unwrap(); let file = file.unwrap();
music.push(Music::from_file(file.path())); music.push(Music::from_file(file.path()));
println!("music found: {:?}", file.file_name());
} }
} }
@@ -147,19 +168,50 @@ impl Game {
for file in palette_files { for file in palette_files {
let file = file.unwrap(); let file = file.unwrap();
palettes.push(Palette::from_file(file.path())); palettes.push(Palette::from_file(file.path()));
println!("palette found: {:?}", file.file_name());
} }
let mut images_dir = path.clone(); let mut images_dir = path.clone();
images_dir.push("palettes"); images_dir.push("images");
let image_files = images_dir.read_dir() let image_files = images_dir.read_dir()
.expect("couldn't read images dir"); .expect("couldn't read image dir");
for file in image_files { for file in image_files {
let file = file.unwrap(); let file = file.unwrap();
images.push(Image::from_file(file.path())); images.push(Image::from_file(file.path()));
println!("image found: {:?}", file.file_name()); }
let mut tiles_dir = path.clone();
tiles_dir.push("tiles");
let tiles_files = tiles_dir.read_dir()
.expect("couldn't read tile dir");
for file in tiles_files {
let file = file.unwrap();
tiles.push(Tile::from_file(file.path()));
}
let mut entities_dir = path.clone();
entities_dir.push("entities");
let entities_files = entities_dir.read_dir()
.expect("couldn't read tile dir");
for file in entities_files {
let file = file.unwrap();
entities.push(Entity::from_file(file.path()));
}
let mut scenes_dir = path.clone();
scenes_dir.push("scenes");
let scenes_files = scenes_dir.read_dir()
.expect("couldn't read scene dir");
for file in scenes_files {
let file = file.unwrap();
scenes.push(Scene::from_file(file.path()));
} }
let mut game_config = path.clone(); let mut game_config = path.clone();
@@ -169,6 +221,110 @@ impl Game {
let config: Config = toml::from_str(&config) let config: Config = toml::from_str(&config)
.expect("Couldn't parse game config"); .expect("Couldn't parse game config");
Ok(Game { config, images, palettes, music }) Ok(Game { config, images, tiles, palettes, music, entities, scenes })
}
pub fn get_image_by_name(&self, name: &String) -> Option<&Image> {
for image in self.images.iter() {
if &image.name == name {
return Some(&image);
}
}
None
}
pub fn get_entities_by_tag(&self, tag: &String) -> Vec<&Entity> {
let mut entities = Vec::new();
for entity in self.entities.iter() {
if entity.tags.contains(tag) {
entities.push(entity);
}
}
entities
}
// todo Result<&Entity>?
pub fn get_entity_by_name(&self, name: String) -> Option<&Entity> {
for entity in self.entities.iter() {
if entity.name == name {
return Some(entity);
}
}
None
}
// todo Result<&Music>?
pub fn get_music_by_name(&self, name: String) -> Option<&Music> {
for music in self.music.iter() {
if music.name == name {
return Some(music);
}
}
None
}
// todo Result<&Palette>?
pub fn find_palette(&mut self, name: &str) -> Option<&mut Palette> {
for palette in self.palettes.iter_mut() {
if palette.name == name {
return Some(palette);
}
}
None
}
// todo Result<&Tile>?
pub fn get_tile_by_name(&self, name: String) -> Option<&Tile> {
for tile in self.tiles.iter() {
if tile.name == name {
return Some(tile);
}
}
None
}
pub fn get_scene_by_name(&mut self, name: String) -> Option<&mut Scene> {
for scene in self.scenes.iter_mut() {
if scene.name == name {
return Some(scene);
}
}
None
}
pub fn remove_entity(&mut self, scene_name: String, position: Position) {
let width = self.config.width.clone();
self.get_scene_by_name(scene_name).unwrap()
.foreground[position.to_index(width) as usize] = None;
}
}
#[cfg(test)]
mod test {
use crate::{Position, Game};
#[test]
fn position_to_index() {
assert_eq!(Position { x: 1, y: 5 }.to_index(8), 41);
assert_eq!(Position { x: 0, y: 0 }.to_index(8), 0);
}
#[test]
fn remove_entity() {
let mut game = Game::new();
game.scenes.push(crate::mock::scenes::zero());
game.remove_entity("zero".into(), Position { x: 1, y: 1 });
assert_eq!(game.scenes[0].foreground[9], None);
} }
} }

View File

@@ -1,7 +1,7 @@
pub(crate) mod image { pub mod image {
use crate::image::Image; use crate::image::Image;
pub fn bg() -> Image { pub fn _bg() -> Image {
Image { Image {
name: "bg".to_string(), name: "bg".to_string(),
pixels: vec![ pixels: vec![
@@ -17,7 +17,7 @@ pub(crate) mod image {
} }
} }
pub fn block() -> Image { pub fn _block() -> Image {
Image { Image {
name: "block".to_string(), name: "block".to_string(),
pixels: vec![ pixels: vec![
@@ -49,7 +49,7 @@ pub(crate) mod image {
} }
} }
pub fn cat() -> Image { pub fn _cat() -> Image {
Image { Image {
name: "cat".to_string(), name: "cat".to_string(),
pixels: vec![ pixels: vec![
@@ -66,22 +66,21 @@ pub(crate) mod image {
} }
} }
pub(crate) mod palette { pub mod palette {
use crate::{Palette, Colour}; use crate::{Palette, Colour};
pub(crate) fn default() -> Palette { pub(crate) fn default() -> Palette {
Palette { Palette {
name: "blueprint".to_string(), name: "blueprint".to_string(),
colours: vec![ colours: vec![
Colour { red: 0, green: 0, blue: 0 }, Colour { red: 0, green: 82, blue: 204 },
Colour { red: 0, green: 81, blue: 104 }, Colour { red: 128, green: 159, blue: 255 },
Colour { red: 118, green: 159, blue: 155 }, Colour { red: 255, green: 255, blue: 255 },
Colour { red: 155, green: 155, blue: 155 },
], ],
} }
} }
pub(crate) fn soup11() -> Palette { pub fn soup11() -> Palette {
Palette { Palette {
name: "soup11".into(), name: "soup11".into(),
colours: vec![ colours: vec![
@@ -101,10 +100,10 @@ pub(crate) mod palette {
} }
} }
pub(crate) mod scenes { pub mod scenes {
use crate::Scene; use crate::Scene;
pub(crate) fn zero() -> Scene { pub fn zero() -> Scene {
Scene { Scene {
name: "zero".into(), name: "zero".into(),
background: vec![ background: vec![
@@ -146,3 +145,65 @@ pub(crate) mod scenes {
} }
} }
} }
pub mod entities {
use crate::Entity;
pub fn bitsy_avatar() -> Entity {
Entity {
name: "".to_string(),
image: "avatar".to_string(),
tags: vec!["player".into()]
}
}
pub fn bitsy_cat() -> Entity {
Entity {
name: "cat".to_string(),
image: "cat".to_string(),
tags: vec![]
}
}
}
pub mod tiles {
use crate::Tile;
pub fn bitsy_block() -> Tile {
Tile {
name: "block".into(),
images: vec!["block".into()],
wall: false
}
}
}
pub mod game {
use crate::{Config, Game};
pub fn bitsy() -> Game {
Game {
config: Config {
name: Some("Write your game's title here".into()),
width: 16,
height: 16,
tick: 400,
starting_room: None,
version: (0, 1)
},
entities: vec![
crate::mock::entities::bitsy_avatar(),
crate::mock::entities::bitsy_cat(),
],
images: vec![],
palettes: vec![],
scenes: vec![
crate::mock::scenes::zero(),
],
tiles: vec![
crate::mock::tiles::bitsy_block(),
],
music: vec![]
}
}
}

View File

@@ -13,7 +13,24 @@ pub struct Palette {
} }
impl Palette { impl Palette {
/// todo result /// if trying to get an out-of-bounds index, colours will wrap around
/// so if palette has 8 colours (0-7) and index is 8, will return 0
pub fn get_colour(&self, index: &u8) -> Option<&Colour> {
self.colours.get((index % self.colours.len() as u8) as usize)
}
/// colour 0 is transparent
pub fn get_colour_rgba8(&self, index: &u8) -> Vec<u8> {
match index {
0 => vec![0,0,0,0],
_ => {
let colour = self.get_colour(index).unwrap();
vec![colour.red, colour.green, colour.blue, 255]
}
}
}
/// todo Result<Palette>
pub fn from_file(path: PathBuf) -> Self { pub fn from_file(path: PathBuf) -> Self {
match path.extension().unwrap().to_str().unwrap() { match path.extension().unwrap().to_str().unwrap() {
"gpl" => Self::from_gpl(path), "gpl" => Self::from_gpl(path),
@@ -84,8 +101,11 @@ impl Palette {
) )
}).collect(); }).collect();
// todo re-insert original comments // todo fix palette description? does it matter?
format!("GIMP Palette\r\n{}\r\n", colours.join("\r\n")) format!(
"GIMP Palette\r\n#Palette Name: {}\r\n#Colors: {}\r\n{}\r\n",
self.name, colours.len(), colours.join("\r\n")
)
} }
/// Paint.net .txt format /// Paint.net .txt format
@@ -157,6 +177,7 @@ impl Palette {
Self { name, colours } Self { name, colours }
} }
/// todo maybe this should be Into<DynamicImage>
pub fn to_png(&self) -> DynamicImage { pub fn to_png(&self) -> DynamicImage {
let mut image = DynamicImage::new_rgb8( let mut image = DynamicImage::new_rgb8(
self.colours.len() as u32, 1 as u32 self.colours.len() as u32, 1 as u32
@@ -174,29 +195,13 @@ impl Palette {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::path::PathBuf; use std::path::PathBuf;
use crate::{Colour, Palette}; use crate::Palette;
#[test] #[test]
fn palette_from_jasc() { fn palette_from_jasc() {
let path = PathBuf::from("src/test-resources/basic/palettes/soup11.pal"); let path = PathBuf::from("src/test-resources/palettes/soup11.pal");
let output = Palette::from_jasc(path); let output = Palette::from_jasc(path);
let expected = crate::mock::palette::soup11();
let expected = Palette {
name: "soup11".into(),
colours: vec![
Colour { red: 79, green: 30, blue: 69 },
Colour { red: 150, green: 48, blue: 87 },
Colour { red: 215, green: 68, blue: 89 },
Colour { red: 235, green: 112, blue: 96 },
Colour { red: 255, green: 179, blue: 131 },
Colour { red: 255, green: 255, blue: 255 },
Colour { red: 127, green: 227, blue: 187 },
Colour { red: 92, green: 187, blue: 196 },
Colour { red: 69, green: 126, blue: 163 },
Colour { red: 56, green: 66, blue: 118 },
Colour { red: 50, green: 36, blue: 81 }
]
};
assert_eq!(output, expected); assert_eq!(output, expected);
} }
@@ -210,7 +215,7 @@ mod test {
#[test] #[test]
fn palette_from_gpl() { fn palette_from_gpl() {
let path = PathBuf::from("src/test-resources/basic/palettes/soup11.gpl"); let path = PathBuf::from("src/test-resources/palettes/soup11.gpl");
let output = Palette::from_gpl(path); let output = Palette::from_gpl(path);
let expected = crate::mock::palette::soup11(); let expected = crate::mock::palette::soup11();
@@ -227,7 +232,7 @@ mod test {
#[test] #[test]
fn palette_from_txt() { fn palette_from_txt() {
let path = PathBuf::from("src/test-resources/basic/palettes/soup11.txt"); let path = PathBuf::from("src/test-resources/palettes/soup11.txt");
let output = Palette::from_txt(path); let output = Palette::from_txt(path);
let expected = crate::mock::palette::soup11(); let expected = crate::mock::palette::soup11();
assert_eq!(output, expected); assert_eq!(output, expected);
@@ -243,7 +248,7 @@ mod test {
#[test] #[test]
fn palette_from_hex() { fn palette_from_hex() {
let path = PathBuf::from("src/test-resources/basic/palettes/soup11.hex"); let path = PathBuf::from("src/test-resources/palettes/soup11.hex");
let output = Palette::from_hex(path); let output = Palette::from_hex(path);
let expected = crate::mock::palette::soup11(); let expected = crate::mock::palette::soup11();
assert_eq!(output, expected); assert_eq!(output, expected);
@@ -258,7 +263,7 @@ mod test {
#[test] #[test]
fn palette_from_png() { fn palette_from_png() {
let path = PathBuf::from("src/test-resources/basic/palettes/soup11.png"); let path = PathBuf::from("src/test-resources/palettes/soup11.png");
let output = Palette::from_png(path); let output = Palette::from_png(path);
let expected = crate::mock::palette::soup11(); let expected = crate::mock::palette::soup11();
assert_eq!(output, expected); assert_eq!(output, expected);
@@ -269,7 +274,7 @@ mod test {
use image::io::Reader as ImageReader; use image::io::Reader as ImageReader;
let output = crate::mock::palette::soup11().to_png(); let output = crate::mock::palette::soup11().to_png();
let path = PathBuf::from("src/test-resources/basic/palettes/soup11.png"); let path = PathBuf::from("src/test-resources/palettes/soup11.png");
let expected = ImageReader::open(path).unwrap().decode().unwrap(); let expected = ImageReader::open(path).unwrap().decode().unwrap();
assert_eq!(output, expected); assert_eq!(output, expected);
} }

View File

@@ -1,6 +1,7 @@
use std::fs::read_to_string; use std::fs::read_to_string;
use std::path::PathBuf; use std::path::PathBuf;
use serde_derive::{Serialize, Deserialize}; use serde_derive::{Serialize, Deserialize};
use crate::Position;
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub struct Scene { pub struct Scene {
@@ -36,6 +37,10 @@ impl Scene {
Self { name, background, foreground } Self { name, background, foreground }
} }
pub fn set_tile(_position: Position, _new: String) {
todo!();
}
} }
/// scene name is derived from the filename, /// scene name is derived from the filename,

View File

@@ -0,0 +1,2 @@
image = "avatar"
tags = ["player"]

View File

@@ -0,0 +1,2 @@
image = "cat"
tags = []

View File

@@ -0,0 +1,2 @@
image = "tea"
tags = []

View File

@@ -0,0 +1,8 @@
00000000
00000000
00000000
00111100
01100100
00100100
00011000
00000000

View File

@@ -1 +0,0 @@
images = ["avatar"]

View File

@@ -0,0 +1,2 @@
images = ["block"]
wall = false

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

View File

@@ -9,6 +9,7 @@ pub struct Tile {
/// todo should there be animation options? reverse, random, etc? /// todo should there be animation options? reverse, random, etc?
/// todo do we need a "current frame" property or leave that up to the player implementation? /// todo do we need a "current frame" property or leave that up to the player implementation?
pub images: Vec<String>, pub images: Vec<String>,
pub wall: bool,
} }
impl Tile { impl Tile {
@@ -19,13 +20,14 @@ impl Tile {
&read_to_string(path).unwrap() &read_to_string(path).unwrap()
).unwrap(); ).unwrap();
Tile { name, images: intermediate.images } Tile { name, images: intermediate.images, wall: intermediate.wall }
} }
} }
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
struct IntermediateTile { struct IntermediateTile {
images: Vec<String>, images: Vec<String>,
wall: bool,
} }
#[cfg(test)] #[cfg(test)]
@@ -35,9 +37,15 @@ mod test {
#[test] #[test]
fn from_file() { fn from_file() {
let path = PathBuf::from("src/test-resources/basic/tiles/avatar.toml"); let path = PathBuf::from("src/test-resources/basic/tiles/block.toml");
let output = Tile::from_file(path); let output = Tile::from_file(path);
let expected = Tile { name: "avatar".into(), images: vec!["avatar".to_string()] };
let expected = Tile {
name: "block".into(),
images: vec!["block".to_string()],
wall: false
};
assert_eq!(output, expected); assert_eq!(output, expected);
} }
} }