implement blueprint creation, placement, saving and loading

This commit is contained in:
Crispy 2024-10-10 16:58:50 +02:00
parent 3e4eb21e5e
commit e22f568d2f
9 changed files with 281 additions and 40 deletions

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

BIN
assets/cancel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

BIN
assets/save.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

60
src/blueprint.rs Normal file
View 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}");
}
}
}

View file

@ -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,38 +684,42 @@ 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);
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')));
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, 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(
(1, 1),
&Tile::Powerable(PTile::Wire(self.tool_wire), false).texture(),
Tool::Wire,
);
tool_button((1, 0), "marble", Tool::SetTile(Tile::from_char('o')));
tool_button(
(1, 1),
&Tile::Powerable(PTile::Wire(self.tool_wire), false).texture(),
Tool::Wire,
);
tool_button((1, 2), &Tile::Arrow(self.tool_arrow).texture(), Tool::Arrow);
tool_button(
(1, 3),
&Tile::Mirror(self.tool_mirror).texture(),
Tool::Mirror,
);
tool_button(
(1, 4),
&Tile::Powerable(PTile::Math(self.tool_math), false).texture(),
Tool::Math,
);
tool_button(
(1, 5),
&Tile::Powerable(PTile::Gate(self.tool_gate), false).texture(),
Tool::Gate,
);
tool_button((1, 2), &Tile::Arrow(self.tool_arrow).texture(), Tool::Arrow);
tool_button(
(1, 3),
&Tile::Mirror(self.tool_mirror).texture(),
Tool::Mirror,
);
tool_button(
(1, 4),
&Tile::Powerable(PTile::Math(self.tool_math), false).texture(),
Tool::Math,
);
tool_button(
(1, 5),
&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
}

View file

@ -5,6 +5,7 @@ use std::{
use raylib::prelude::*;
mod blueprint;
mod editor;
mod level;
mod marble_engine;

View file

@ -139,7 +139,7 @@ impl Machine {
if let Tile::Powerable(PTile::Bag, _) = front_tile {
return Event::Remove;
}
if let Tile::Powerable(PTile::IO, _) = front_tile{
if let Tile::Powerable(PTile::IO, _) = front_tile {
self.output.push(value as u8);
return Event::Remove;
}

View file

@ -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>>,