From e22f568d2f74e01a382563e23ba780f994533f19 Mon Sep 17 00:00:00 2001 From: CrispyPin Date: Thu, 10 Oct 2024 16:58:50 +0200 Subject: [PATCH] implement blueprint creation, placement, saving and loading --- README.md | 6 +- assets/blueprint.png | Bin 0 -> 298 bytes assets/cancel.png | Bin 0 -> 197 bytes assets/save.png | Bin 0 -> 177 bytes src/blueprint.rs | 60 ++++++++++ src/editor.rs | 240 +++++++++++++++++++++++++++++++------ src/main.rs | 1 + src/marble_engine.rs | 2 +- src/marble_engine/board.rs | 12 ++ 9 files changed, 281 insertions(+), 40 deletions(-) create mode 100644 assets/blueprint.png create mode 100644 assets/cancel.png create mode 100644 assets/save.png create mode 100644 src/blueprint.rs diff --git a/README.md b/README.md index 87e66a6..9dfaf47 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/assets/blueprint.png b/assets/blueprint.png new file mode 100644 index 0000000000000000000000000000000000000000..490d049e86f50f8a5a85ec135d224aece3c05156 GIT binary patch literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`A3a?hLo9le6BY;<7#SEC_{9DH zWY5gZe0XED3{O~=y+lXqbG9zyO$U6e%DS0b)sD1D9%|CDyze42P25;G#dM|Tlcl#Mz)DA0n({DRqTyp8lOWqXIqSm#hmW1V@aeU(Gq1FL zn_5$x!?im=Ue_W2_C*0dEcP~>6_}Bh;#_cyX;az^^B-@fExZ5}^fl&LkbUeG>kPG$ nvo{#p#1oi+e(6XG=4D`b5#FT6siS2C^gM&7tDnm{r-UW|dR%T( literal 0 HcmV?d00001 diff --git a/assets/cancel.png b/assets/cancel.png new file mode 100644 index 0000000000000000000000000000000000000000..107848aef27768a9387253484ef40c096f7b7285 GIT binary patch literal 197 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`J)SO(Ar`%BCmrN%P!M1dkN%ME z#2(Qh8q87R@TgzvgX@vUea?^V9;Pwr9FXTcH}$i{so6gt#4t82)q6I@KzU_C??UO- zTW{<&x{JZn)z4*}Q$iB}wZ%o0 literal 0 HcmV?d00001 diff --git a/assets/save.png b/assets/save.png new file mode 100644 index 0000000000000000000000000000000000000000..7f196ee44cd569b62029a8dde2159489906ded15 GIT binary patch literal 177 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`6`n4RAr`&K2@Avu3~ztCe>CTI zz0&~@nNsUxaCQezndyDL<0qyqlyF$rt!c!-7Qvx;qxRBd6Xpqjy?7Sf;J(7&yol|O z)UOLq0%IIJjLl3X^eX=RFpOhPNlpDI-LS4fU_l@Ew>h7ZT2y_LVmCaWBCb5K?+8DG YSgniv4r7;ipc@!GUHx3vIVCg!01QV#UjP6A literal 0 HcmV?d00001 diff --git a/src/blueprint.rs b/src/blueprint.rs new file mode 100644 index 0000000..3c99933 --- /dev/null +++ b/src/blueprint.rs @@ -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, +} + +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}"); + } + } +} diff --git a/src/editor.rs b/src/editor.rs index 54d6412..8692957 100644 --- a/src/editor.rs +++ b/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, + blueprints: Vec, + 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 { + let mut blueprints = Vec::::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 +} diff --git a/src/main.rs b/src/main.rs index bc86108..06ecfc6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use std::{ use raylib::prelude::*; +mod blueprint; mod editor; mod level; mod marble_engine; diff --git a/src/marble_engine.rs b/src/marble_engine.rs index 06e992d..bff51b6 100644 --- a/src/marble_engine.rs +++ b/src/marble_engine.rs @@ -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; } diff --git a/src/marble_engine/board.rs b/src/marble_engine/board.rs index 0d96a95..0a2f18d 100644 --- a/src/marble_engine/board.rs +++ b/src/marble_engine/board.rs @@ -1,3 +1,5 @@ +use std::ops::Add; + use crate::{draw_scaled_texture, Textures}; use super::tile::*; @@ -50,6 +52,16 @@ impl From 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>,