implement blueprint creation, placement, saving and loading
This commit is contained in:
parent
3e4eb21e5e
commit
e22f568d2f
9 changed files with 281 additions and 40 deletions
|
@ -6,11 +6,13 @@ logic mostly like https://git.crispypin.cc/CrispyPin/marble
|
|||
## todo
|
||||
(more levels)
|
||||
story/lore
|
||||
blueprints
|
||||
timestamps in solutions and blueprints
|
||||
multiple input/output sets
|
||||
scroll level list
|
||||
scroll blueprint list
|
||||
make marble movement more consistent (`>o o<` depends on internal marble order)
|
||||
decide on marble data size (u32 or byte?)
|
||||
blueprint rotation
|
||||
blueprint rotation?
|
||||
|
||||
## file hierarchy
|
||||
```
|
||||
|
|
BIN
assets/blueprint.png
Normal file
BIN
assets/blueprint.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 298 B |
BIN
assets/cancel.png
Normal file
BIN
assets/cancel.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 197 B |
BIN
assets/save.png
Normal file
BIN
assets/save.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 177 B |
60
src/blueprint.rs
Normal file
60
src/blueprint.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use std::{
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{marble_engine::board::Board, userdata_dir};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Blueprint {
|
||||
id: String,
|
||||
pub name: String,
|
||||
pub board: String,
|
||||
#[serde(skip, default)]
|
||||
tile_board: Option<Board>,
|
||||
}
|
||||
|
||||
impl Blueprint {
|
||||
pub fn new(content: &Board, number: usize) -> Self {
|
||||
Self {
|
||||
id: format!("blueprint_{number}"),
|
||||
name: format!("Blueprint {number}"),
|
||||
board: content.to_string(),
|
||||
tile_board: Some(content.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_board(&mut self) -> &Board {
|
||||
if self.tile_board.is_none() {
|
||||
self.tile_board = Some(Board::parse(&self.board));
|
||||
}
|
||||
self.tile_board.as_ref().unwrap()
|
||||
}
|
||||
|
||||
pub fn get_board(&self) -> Option<&Board> {
|
||||
self.tile_board.as_ref()
|
||||
}
|
||||
|
||||
fn path(&self) -> PathBuf {
|
||||
let dir = userdata_dir().join("blueprints");
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
dir.join(format!("{}.json", &self.id))
|
||||
}
|
||||
|
||||
pub fn save(&self) {
|
||||
let path = self.path();
|
||||
let json = serde_json::to_string_pretty(self).unwrap();
|
||||
let mut file = File::create(path).unwrap();
|
||||
file.write_all(json.as_bytes()).unwrap();
|
||||
}
|
||||
|
||||
pub fn remove_file(&self) {
|
||||
let path = self.path();
|
||||
if let Err(e) = fs::remove_file(path) {
|
||||
eprint!("Error removing blueprint file: {e}");
|
||||
}
|
||||
}
|
||||
}
|
188
src/editor.rs
188
src/editor.rs
|
@ -1,8 +1,13 @@
|
|||
use std::{mem::transmute, ops::Rem};
|
||||
use std::{
|
||||
fs::{read_dir, read_to_string},
|
||||
mem::transmute,
|
||||
ops::Rem,
|
||||
};
|
||||
|
||||
use raylib::prelude::*;
|
||||
|
||||
use crate::{
|
||||
blueprint::Blueprint,
|
||||
draw_scaled_texture, draw_usize,
|
||||
level::Level,
|
||||
marble_engine::{
|
||||
|
@ -10,13 +15,14 @@ use crate::{
|
|||
tile::{Direction, GateType, MathOp, MirrorType, PTile, Tile, WireType},
|
||||
Machine,
|
||||
},
|
||||
simple_button, slider,
|
||||
simple_button, simple_option_button, slider,
|
||||
solution::{Score, Solution},
|
||||
text_input, texture_option_button, Textures,
|
||||
text_input, texture_option_button, userdata_dir, Textures,
|
||||
};
|
||||
|
||||
const HEADER_HEIGHT: i32 = 40;
|
||||
const FOOTER_HEIGHT: i32 = 95;
|
||||
const SIDEBAR_WIDTH: i32 = 200 + 32 * 2 + 5 * 4;
|
||||
const MAX_ZOOM_IN: i32 = 3;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -36,6 +42,8 @@ pub struct Editor {
|
|||
tool_mirror: MirrorType,
|
||||
tool_wire: WireType,
|
||||
input_text_selected: bool,
|
||||
new_blueprint_name: String,
|
||||
blueprint_name_selected: bool,
|
||||
sim_speed: u8,
|
||||
time_since_step: f32,
|
||||
exit_state: ExitState,
|
||||
|
@ -43,6 +51,8 @@ pub struct Editor {
|
|||
complete_popup: Popup,
|
||||
// fail_popup: Popup,
|
||||
score: Option<Score>,
|
||||
blueprints: Vec<Blueprint>,
|
||||
selected_blueprint: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
@ -64,6 +74,7 @@ enum Tool {
|
|||
Arrow,
|
||||
Mirror,
|
||||
SelectArea(Option<(Pos, Pos)>, bool),
|
||||
Blueprint,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
@ -93,6 +104,8 @@ impl Editor {
|
|||
output_as_text: level.output_is_text(),
|
||||
input_as_text: level.input_is_text(),
|
||||
input_text_selected: false,
|
||||
new_blueprint_name: String::new(),
|
||||
blueprint_name_selected: false,
|
||||
sim_speed: 8,
|
||||
time_since_step: 0.,
|
||||
tool_math: MathOp::Add,
|
||||
|
@ -106,6 +119,8 @@ impl Editor {
|
|||
complete_popup: Popup::Start,
|
||||
// fail_popup: Popup::Start,
|
||||
score: solution.score,
|
||||
blueprints: get_blueprints(),
|
||||
selected_blueprint: usize::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,7 +224,8 @@ impl Editor {
|
|||
| Tool::Erase
|
||||
| Tool::SetTile(_)
|
||||
| Tool::Digits(_)
|
||||
| Tool::SelectArea(_, _) => (),
|
||||
| Tool::SelectArea(_, _)
|
||||
| Tool::Blueprint => (),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -219,8 +235,8 @@ impl Editor {
|
|||
let tile_y = self.source_board.height() as f32 / 2. * tile_size;
|
||||
let screen_x = d.get_screen_width() as f32 / 2.;
|
||||
let screen_y = d.get_screen_height() as f32 / 2.;
|
||||
self.view_offset.x = screen_x - tile_x;
|
||||
self.view_offset.y = screen_y - tile_y;
|
||||
self.view_offset.x = (screen_x - tile_x).floor();
|
||||
self.view_offset.y = (screen_y - tile_y).floor();
|
||||
}
|
||||
|
||||
fn change_zoom_level(&mut self, d: &RaylibHandle, delta: i32) {
|
||||
|
@ -246,9 +262,31 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_tile(&mut self, mut pos: Pos, tile: Tile) {
|
||||
fn save_blueprint(&mut self, selection: (Pos, Pos)) {
|
||||
let min = selection.0.min(selection.1);
|
||||
let max = selection.0.max(selection.1) + (1, 1).into();
|
||||
let width = (max.x - min.x) as usize;
|
||||
let height = (max.y - min.y) as usize;
|
||||
let mut board = Board::new_empty(width, height);
|
||||
for (target_x, x) in (min.x..=max.x).enumerate() {
|
||||
for (target_y, y) in (min.y..=max.y).enumerate() {
|
||||
if let Some(tile) = self.source_board.get(Pos { x, y }) {
|
||||
board.set((target_x, target_y).into(), tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut blueprint = Blueprint::new(&board, self.blueprints.len());
|
||||
if !self.new_blueprint_name.is_empty() {
|
||||
blueprint.name = self.new_blueprint_name.clone();
|
||||
}
|
||||
blueprint.save();
|
||||
self.blueprints.push(blueprint);
|
||||
self.active_tool = Tool::Blueprint;
|
||||
}
|
||||
|
||||
fn grow_board_and_move_view(&mut self, pos: &mut Pos) {
|
||||
let tile_size = (16 << self.zoom) as f32;
|
||||
let (x, y) = self.source_board.grow_to_include(pos);
|
||||
let (x, y) = self.source_board.grow_to_include(*pos);
|
||||
if x != 0 || y != 0 {
|
||||
self.view_offset.x -= x as f32 * tile_size;
|
||||
self.view_offset.y -= y as f32 * tile_size;
|
||||
|
@ -268,6 +306,11 @@ impl Editor {
|
|||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_tile(&mut self, mut pos: Pos, tile: Tile) {
|
||||
let tile_size = (16 << self.zoom) as f32;
|
||||
self.grow_board_and_move_view(&mut pos);
|
||||
self.source_board.set(pos, tile);
|
||||
if tile.is_blank() {
|
||||
let (x, y) = self.source_board.trim_size();
|
||||
|
@ -312,7 +355,7 @@ impl Editor {
|
|||
self.zoom_out(rl);
|
||||
}
|
||||
if rl.is_mouse_button_down(MouseButton::MOUSE_BUTTON_MIDDLE) {
|
||||
self.view_offset += rl.get_mouse_delta()
|
||||
self.view_offset += rl.get_mouse_delta();
|
||||
}
|
||||
if rl.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_RIGHT) {
|
||||
self.center_view(rl);
|
||||
|
@ -359,6 +402,52 @@ impl Editor {
|
|||
self.draw_bottom_bar(d, textures);
|
||||
self.draw_top_bar(d, textures);
|
||||
|
||||
if self.active_tool == Tool::Blueprint {
|
||||
let sidebar_height = d.get_screen_height() - FOOTER_HEIGHT - HEADER_HEIGHT - 40;
|
||||
d.draw_rectangle(
|
||||
0,
|
||||
HEADER_HEIGHT + 20,
|
||||
SIDEBAR_WIDTH,
|
||||
sidebar_height,
|
||||
Color::new(32, 32, 32, 255),
|
||||
);
|
||||
d.draw_text("Blueprints", 10, HEADER_HEIGHT + 30, 20, Color::WHITE);
|
||||
let mut y = HEADER_HEIGHT + 60;
|
||||
for (i, b) in self.blueprints.iter_mut().enumerate() {
|
||||
if simple_button(d, 5, y, 32, 32) {
|
||||
b.remove_file();
|
||||
self.blueprints.remove(i);
|
||||
break;
|
||||
}
|
||||
draw_scaled_texture(d, textures.get("cancel"), 5, y, 2.);
|
||||
let is_selected = self.selected_blueprint == i;
|
||||
let mut text_selected = is_selected && self.blueprint_name_selected;
|
||||
text_input(
|
||||
d,
|
||||
Rectangle::new(42., y as f32, 200., 32.),
|
||||
&mut b.name,
|
||||
&mut text_selected,
|
||||
32,
|
||||
is_selected,
|
||||
);
|
||||
if is_selected {
|
||||
self.blueprint_name_selected = text_selected;
|
||||
}
|
||||
simple_option_button(d, 42 + 205, y, 32, 32, i, &mut self.selected_blueprint);
|
||||
|
||||
d.draw_texture_ex(
|
||||
textures.get("blueprint"),
|
||||
Vector2::new((42 + 205) as f32, y as f32),
|
||||
0.,
|
||||
2.,
|
||||
Color::new(255, 255, 255, if is_selected { 255 } else { 150 }),
|
||||
);
|
||||
|
||||
// d.draw_text(&b.name, 15, y+5, 20, Color::WHITE);
|
||||
y += 37;
|
||||
}
|
||||
}
|
||||
|
||||
if self.complete_popup == Popup::Visible {
|
||||
let width = 310;
|
||||
let height = 165;
|
||||
|
@ -555,6 +644,27 @@ impl Editor {
|
|||
Color::new(32, 32, 32, 255),
|
||||
);
|
||||
|
||||
let mut hide_tile_tools = false;
|
||||
if let Tool::SelectArea(Some(selection), _) = self.active_tool {
|
||||
hide_tile_tools = true;
|
||||
text_input(
|
||||
d,
|
||||
Rectangle::new(100., footer_top + 10., 240., 30.),
|
||||
&mut self.new_blueprint_name,
|
||||
&mut self.blueprint_name_selected,
|
||||
32,
|
||||
true,
|
||||
);
|
||||
if simple_button(d, 100, footer_top as i32 + 49, 40, 40) {
|
||||
self.save_blueprint(selection);
|
||||
}
|
||||
draw_scaled_texture(d, textures.get("save"), 104, footer_top as i32 + 53, 2.);
|
||||
if simple_button(d, 144, footer_top as i32 + 49, 40, 40) {
|
||||
self.active_tool = Tool::SelectArea(None, false);
|
||||
}
|
||||
draw_scaled_texture(d, textures.get("cancel"), 148, footer_top as i32 + 53, 2.);
|
||||
}
|
||||
|
||||
let mut tool_button = |(row, col): (i32, i32), texture: &str, tool_option: Tool| {
|
||||
let border = 4.;
|
||||
let gap = 2.;
|
||||
|
@ -574,14 +684,17 @@ impl Editor {
|
|||
};
|
||||
tool_button((0, -2), "eraser", Tool::Erase);
|
||||
tool_button((1, -2), "selection", Tool::SelectArea(None, false));
|
||||
tool_button((0, -1), "digit_tool", Tool::Digits(None));
|
||||
|
||||
tool_button((0, -1), "blueprint", Tool::Blueprint);
|
||||
tool_button((1, -1), "transparent", Tool::None);
|
||||
|
||||
if !hide_tile_tools {
|
||||
tool_button((0, 0), "block", Tool::SetTile(Tile::from_char('#')));
|
||||
tool_button((0, 1), "bag_off", Tool::SetTile(Tile::from_char('B')));
|
||||
tool_button((0, 2), "trigger_off", Tool::SetTile(Tile::from_char('*')));
|
||||
tool_button((0, 3), "io_tile_off", Tool::SetTile(Tile::from_char('I')));
|
||||
tool_button((0, 5), "flipper_off", Tool::SetTile(Tile::from_char('F')));
|
||||
tool_button((0, 4), "flipper_off", Tool::SetTile(Tile::from_char('F')));
|
||||
tool_button((0, 5), "digit_tool", Tool::Digits(None));
|
||||
|
||||
tool_button((1, 0), "marble", Tool::SetTile(Tile::from_char('o')));
|
||||
tool_button(
|
||||
|
@ -606,6 +719,7 @@ impl Editor {
|
|||
&Tile::Powerable(PTile::Gate(self.tool_gate), false).texture(),
|
||||
Tool::Gate,
|
||||
);
|
||||
}
|
||||
|
||||
let output_x = 370;
|
||||
let output_cell_width = 43;
|
||||
|
@ -719,6 +833,7 @@ impl Editor {
|
|||
Tool::Digits(_) => "selection".into(),
|
||||
Tool::SelectArea(_, false) => "area_full".into(),
|
||||
Tool::SelectArea(_, true) => "transparent".into(),
|
||||
Tool::Blueprint => "transparent".into(),
|
||||
};
|
||||
|
||||
d.draw_texture_ex(
|
||||
|
@ -755,6 +870,26 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
}
|
||||
Tool::Blueprint => {
|
||||
if mouse_pos.x > SIDEBAR_WIDTH as f32 {
|
||||
if let Some(bp) = self.blueprints.get(self.selected_blueprint) {
|
||||
let board = bp.get_board().unwrap().clone();
|
||||
let mut pos = pos;
|
||||
self.grow_board_and_move_view(&mut pos);
|
||||
self.grow_board_and_move_view(
|
||||
&mut (pos + (board.width() - 1, board.height() - 1).into()),
|
||||
);
|
||||
for x in 0..board.width() {
|
||||
for y in 0..board.height() {
|
||||
let p = (x, y).into();
|
||||
if let Some(tile) = board.get(p) {
|
||||
self.source_board.set(p + pos, tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Tool::SelectArea(_, _) => (),
|
||||
}
|
||||
}
|
||||
|
@ -779,6 +914,19 @@ impl Editor {
|
|||
*is_selecting = false;
|
||||
}
|
||||
}
|
||||
if let Tool::Blueprint = self.active_tool {
|
||||
if let Some(bp) = self.blueprints.get_mut(self.selected_blueprint) {
|
||||
let view_offset = Vector2::new(
|
||||
self.view_offset.x.rem(tile_size as f32),
|
||||
self.view_offset.y.rem(tile_size as f32),
|
||||
);
|
||||
let mut offset = mouse_pos - view_offset;
|
||||
offset.x -= offset.x.rem(tile_size as f32);
|
||||
offset.y -= offset.y.rem(tile_size as f32);
|
||||
offset += view_offset;
|
||||
bp.convert_board().draw(d, textures, offset, self.zoom);
|
||||
}
|
||||
}
|
||||
}
|
||||
// draw selection
|
||||
if let Tool::SelectArea(Some((start, end)), _) = self.active_tool {
|
||||
|
@ -809,3 +957,21 @@ impl PartialEq for Tool {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_blueprints() -> Vec<Blueprint> {
|
||||
let mut blueprints = Vec::<Blueprint>::new();
|
||||
let Ok(dir) = read_dir(userdata_dir().join("blueprints")) else {
|
||||
return blueprints;
|
||||
};
|
||||
for d in dir.flatten() {
|
||||
let l = read_to_string(d.path())
|
||||
.ok()
|
||||
.as_deref()
|
||||
.and_then(|s| serde_json::from_str(s).ok());
|
||||
if let Some(level) = l {
|
||||
blueprints.push(level);
|
||||
}
|
||||
}
|
||||
blueprints.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
blueprints
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ use std::{
|
|||
|
||||
use raylib::prelude::*;
|
||||
|
||||
mod blueprint;
|
||||
mod editor;
|
||||
mod level;
|
||||
mod marble_engine;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::ops::Add;
|
||||
|
||||
use crate::{draw_scaled_texture, Textures};
|
||||
|
||||
use super::tile::*;
|
||||
|
@ -50,6 +52,16 @@ impl From<Vector2> for Pos {
|
|||
}
|
||||
}
|
||||
|
||||
impl Add for Pos {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
x: self.x + rhs.x,
|
||||
y: self.y + rhs.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Board {
|
||||
rows: Vec<Vec<Tile>>,
|
||||
|
|
Loading…
Reference in a new issue