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::{ board::Board, pos::Pos, tile::{Direction, GateType, MathOp, MirrorType, PTile, Tile, WireType}, Machine, }, simple_button, simple_option_button, slider, solution::{Score, Solution}, 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; const BOARD_MARGIN: isize = 3; #[derive(Debug)] pub struct Editor { source_board: Board, level: Level, machine: Machine, sim_state: SimState, view_offset: Vector2, zoom: i32, output_as_text: bool, input_as_text: bool, active_tool: Tool, tool_math: MathOp, tool_gate: GateType, tool_arrow: Direction, 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, exit_menu: bool, complete_popup: Popup, // fail_popup: Popup, score: Option, blueprints: Vec, selected_blueprint: usize, } #[derive(Debug, PartialEq)] enum Popup { Start, Visible, Dismissed, } #[derive(Debug, Clone)] enum Tool { None, Erase, SetTile(Tile), Digits(Option), Math, Gate, Wire, Arrow, Mirror, SelectArea(Option<(Pos, Pos)>, bool), Blueprint, } #[derive(Debug, Clone, PartialEq)] enum SimState { Editing, Running, Stepping, } #[derive(Debug, Clone, Copy)] pub enum ExitState { Dont, ExitAndSave, Save, ExitNoSave, } impl Editor { pub fn new(solution: Solution, level: Level) -> Self { Self { source_board: Board::parse(&solution.board), machine: Machine::new_empty(level.inputs().to_owned(), 1), sim_state: SimState::Editing, view_offset: Vector2::zero(), zoom: 1, active_tool: Tool::None, 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, tool_gate: GateType::Equal, tool_arrow: Direction::Right, tool_mirror: MirrorType::Forward, tool_wire: WireType::Vertical, level, exit_state: ExitState::Dont, exit_menu: false, complete_popup: Popup::Start, // fail_popup: Popup::Start, score: solution.score, blueprints: get_blueprints(), selected_blueprint: usize::MAX, } } pub fn get_exit_state(&self) -> ExitState { self.exit_state } pub fn level_id(&self) -> &str { self.level.id() } pub fn source_board(&self) -> &Board { &self.source_board } pub fn score(&self) -> Option { self.score.clone() } fn pos_to_screen(&self, pos: Vector2) -> Vector2 { pos * (16 << self.zoom) as f32 + self.view_offset } fn start_sim(&mut self) { self.machine.reset(); self.machine.set_board(self.source_board.clone()); } fn step_pressed(&mut self) { match self.sim_state { SimState::Editing => { self.start_sim(); self.step(); } SimState::Running => (), SimState::Stepping => self.step(), } self.sim_state = SimState::Stepping; } fn step(&mut self) { self.machine.step(); if self.complete_popup == Popup::Visible { self.complete_popup = Popup::Dismissed; } if !self.level.outputs().is_empty() && self.level.outputs() == self.machine.output() && self.complete_popup == Popup::Start { self.complete_popup = Popup::Visible; self.exit_state = ExitState::Save; self.sim_state = SimState::Stepping; self.score = Some(Score { cycles: self.machine.step_count(), tiles: self.source_board.count_tiles(), area: 0, }); } } fn rotate_tool(&mut self, shift: bool) { match &self.active_tool { Tool::Math => { self.tool_math = match self.tool_math { MathOp::Add => MathOp::Sub, MathOp::Sub => MathOp::Mul, MathOp::Mul => MathOp::Div, MathOp::Div => MathOp::Rem, MathOp::Rem => MathOp::Add, } } Tool::Gate => { self.tool_gate = match self.tool_gate { GateType::LessThan => GateType::GreaterThan, GateType::GreaterThan => GateType::Equal, GateType::Equal => GateType::NotEqual, GateType::NotEqual => GateType::LessThan, } } Tool::Arrow => { self.tool_arrow = if shift { self.tool_arrow.left() } else { self.tool_arrow.right() } } Tool::Mirror => { self.tool_mirror = match self.tool_mirror { MirrorType::Forward => MirrorType::Back, MirrorType::Back => MirrorType::Forward, } } Tool::Wire => { self.tool_wire = match self.tool_wire { WireType::Vertical => WireType::Horizontal, WireType::Horizontal => WireType::Cross, WireType::Cross => WireType::Vertical, } } Tool::None | Tool::Erase | Tool::SetTile(_) | Tool::Digits(_) | Tool::SelectArea(_, _) | Tool::Blueprint => (), } } pub fn center_view(&mut self, d: &RaylibHandle) { let tile_size = (16 << self.zoom) as f32; let tile_x = self.source_board.width() as f32 / 2. * tile_size; 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).floor(); self.view_offset.y = (screen_y - tile_y).floor(); } fn change_zoom_level(&mut self, d: &RaylibHandle, delta: i32) { let tile_size = (16 << self.zoom) as f32; let mouse_pos = d.get_mouse_position(); let tile_pos_of_mouse = (mouse_pos - self.view_offset) / tile_size; self.zoom += delta; let tile_size = (16 << self.zoom) as f32; self.view_offset = mouse_pos - tile_pos_of_mouse * tile_size; self.view_offset.x = self.view_offset.x.floor(); self.view_offset.y = self.view_offset.y.floor(); } fn zoom_in(&mut self, d: &RaylibHandle) { if self.zoom < MAX_ZOOM_IN { self.change_zoom_level(d, 1); } } fn zoom_out(&mut self, d: &RaylibHandle) { if self.zoom > 0 { self.change_zoom_level(d, -1); } } 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); } } } board.trim_size(0); 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_update_view(&mut self, pos: &mut Pos) { let tile_size = (16 << self.zoom) as f32; 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; pos.x += x; pos.y += y; match &mut self.active_tool { Tool::Digits(Some(pos)) => { pos.x += x; pos.y += y; } Tool::SelectArea(Some((start, end)), _) => { start.x += x; start.y += y; end.x += x; end.y += y; } _ => (), } } } fn set_tile(&mut self, mut pos: Pos, tile: Tile) { let tile_size = (16 << self.zoom) as f32; pos.x -= BOARD_MARGIN; pos.y -= BOARD_MARGIN; self.grow_board_and_update_view(&mut pos); pos.x += BOARD_MARGIN * 2; pos.y += BOARD_MARGIN * 2; self.grow_board_and_update_view(&mut pos); pos.x -= BOARD_MARGIN; pos.y -= BOARD_MARGIN; self.source_board.set(pos, tile); if tile.is_blank() { let (x, y) = self.source_board.trim_size(BOARD_MARGIN as usize); self.view_offset.x += x as f32 * tile_size; self.view_offset.y += y as f32 * tile_size; } } pub fn update(&mut self, rl: &RaylibHandle) { if rl.is_key_pressed(KeyboardKey::KEY_ESCAPE) { self.sim_state = SimState::Editing; self.complete_popup = Popup::Start; } if self.sim_state == SimState::Running { self.time_since_step += rl.get_frame_time(); if self.time_since_step > 1. / self.sim_speed as f32 { self.time_since_step = 0.; self.step(); } } if rl.is_key_pressed(KeyboardKey::KEY_SPACE) { self.step_pressed() } if rl.is_key_pressed(KeyboardKey::KEY_ENTER) { match self.sim_state { SimState::Editing => { self.start_sim(); self.sim_state = SimState::Running; } SimState::Running => { self.sim_state = SimState::Editing; self.complete_popup = Popup::Start; } SimState::Stepping => self.sim_state = SimState::Running, } } if rl.get_mouse_wheel_move() > 0. { self.zoom_in(rl); } if rl.get_mouse_wheel_move() < 0. { self.zoom_out(rl); } if rl.is_mouse_button_down(MouseButton::MOUSE_BUTTON_MIDDLE) { self.view_offset += rl.get_mouse_delta(); } if rl.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_RIGHT) { self.center_view(rl); } if rl.is_key_pressed(KeyboardKey::KEY_R) { self.rotate_tool(rl.is_key_down(KeyboardKey::KEY_LEFT_SHIFT)); } } fn draw_board(&self, d: &mut RaylibDrawHandle, textures: &Textures) { if self.sim_state == SimState::Editing { self.source_board .draw(d, textures, self.view_offset, self.zoom); } else { self.machine .board() .draw(d, textures, self.view_offset, self.zoom); self.machine .draw_marble_values(d, self.view_offset, self.zoom); } } pub fn draw(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) { d.clear_background(Color::new(64, 64, 64, 255)); let tile_size = (16 << self.zoom) as f32; let grid_spill_x = (self.view_offset.x).rem(tile_size) - tile_size; let grid_spill_y = (self.view_offset.y).rem(tile_size) - tile_size; d.gui_grid( Rectangle::new( grid_spill_x, grid_spill_y, d.get_screen_width() as f32 * 2., d.get_screen_height() as f32 * 2., ), None, tile_size, 1, ); self.draw_board(d, textures); self.board_overlay(d, textures); 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("rubbish"), 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; let x = d.get_screen_width() / 2 - width / 2; let y = d.get_screen_height() / 2 - height / 2; d.draw_rectangle(x, y, width, height, Color::DIMGRAY); d.draw_text("Level Complete!", x + 10, y + 10, 30, Color::LIME); if let Some(score) = &self.score { d.draw_text("cycles", x + 15, y + 45, 20, Color::WHITE); d.draw_rectangle(x + 10, y + 70, 90, 30, Color::DARKGREEN); d.draw_text( &format!("{}", score.cycles), x + 15, y + 75, 20, Color::WHITE, ); d.draw_text("tiles", x + 115, y + 45, 20, Color::WHITE); d.draw_rectangle(x + 110, y + 70, 90, 30, Color::DARKGREEN); d.draw_text( &format!("{}", score.tiles), x + 115, y + 75, 20, Color::WHITE, ); d.draw_text("area", x + 215, y + 45, 20, Color::WHITE); d.draw_rectangle(x + 210, y + 70, 90, 30, Color::DARKGREEN); d.draw_text( &format!("{}", score.area), x + 215, y + 75, 20, Color::WHITE, ); } if simple_button(d, x + 10, y + 110, 140, 45) { self.complete_popup = Popup::Dismissed; } d.draw_text("continue\nediting", x + 15, y + 115, 20, Color::WHITE); if simple_button(d, x + width / 2 + 5, y + 110, 140, 45) { self.exit_state = ExitState::ExitAndSave; } d.draw_text( "return to\nlevel list", x + width / 2 + 10, y + 115, 20, Color::WHITE, ); } } fn draw_top_bar(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) { // background d.draw_rectangle( 0, 0, d.get_screen_width(), HEADER_HEIGHT, Color::new(32, 32, 32, 255), ); if self.exit_menu { if simple_button(d, 5, 5, 75, 30) { self.exit_menu = false; } d.draw_text("cancel", 10, 10, 20, Color::WHITE); if simple_button(d, 85, 5, 60, 30) { self.exit_state = ExitState::ExitAndSave; } d.draw_text("save", 90, 10, 20, Color::WHITE); if simple_button(d, 150, 5, 80, 30) { self.exit_state = ExitState::ExitNoSave; } d.draw_text("revert", 155, 10, 20, Color::WHITE); } else { if simple_button(d, 5, 5, 75, 30) { self.exit_menu = true; } d.draw_text("exit", 10, 10, 20, Color::WHITE); if simple_button(d, 85, 5, 60, 30) { self.exit_state = ExitState::Save; } d.draw_text("save", 90, 10, 20, Color::WHITE); } match self.sim_state { SimState::Editing => { if simple_button(d, 260, 4, 32, 32) { self.start_sim(); self.sim_state = SimState::Running; } draw_scaled_texture(d, textures.get("play"), 260, 4, 2.); } SimState::Running => { if simple_button(d, 260, 4, 32, 32) { self.sim_state = SimState::Stepping; } draw_scaled_texture(d, textures.get("pause"), 260, 4, 2.); if simple_button(d, 296, 4, 32, 32) { self.sim_state = SimState::Editing; self.complete_popup = Popup::Start; } draw_scaled_texture(d, textures.get("stop"), 296, 4, 2.); } SimState::Stepping => { if simple_button(d, 260, 4, 32, 32) { self.sim_state = SimState::Running; } draw_scaled_texture(d, textures.get("play"), 260, 4, 2.); if simple_button(d, 296, 4, 32, 32) { self.sim_state = SimState::Editing; self.complete_popup = Popup::Start; } draw_scaled_texture(d, textures.get("stop"), 296, 4, 2.); } } if simple_button(d, 332, 4, 32, 32) { self.step_pressed(); } draw_scaled_texture(d, textures.get("step"), 332, 4, 2.); d.draw_text("speed", 368, 6, 10, Color::WHITE); draw_usize(d, textures, self.sim_speed as usize, 398, 4, 2, 1); slider(d, &mut self.sim_speed, 1, 33, 368, 24, 48, 12); draw_usize(d, textures, self.machine.step_count(), 420, 4, 5, 2); d.draw_text("input:", 523, 8, 10, Color::WHITE); if simple_button(d, 520, 20, 35, 15) { self.input_as_text = !self.input_as_text } let input_mode_text = if self.input_as_text { "text" } else { "bytes" }; d.draw_text(input_mode_text, 523, 23, 10, Color::WHITE); let input_x = 560; let width = d.get_screen_width(); if self.input_as_text { let mut input_text = String::new(); for &byte in self.machine.input() { if byte.is_ascii_graphic() || byte == b' ' { input_text.push(byte as char); } else { input_text.push('\\'); } } if text_input( d, Rectangle::new(input_x as f32, 5., (width - input_x - 5) as f32, 30.), &mut input_text, &mut self.input_text_selected, 256, self.level.is_sandbox(), ) { self.machine.set_input(input_text.into_bytes()); } } else { let input_cell_width = 43; let input_cells = (d.get_screen_width() - input_x) as usize / input_cell_width as usize; let input_start = self.machine.input_index().saturating_sub(input_cells); let input_end = input_start + input_cells; for (box_index, index) in (input_start..input_end).enumerate() { let x = input_x + input_cell_width * box_index as i32; let byte = self.machine.input().get(index); d.draw_rectangle(x, 5, input_cell_width - 5, 30, Color::DIMGRAY); let color = if index < self.machine.input_index() { d.draw_rectangle(x + 4, 25, input_cell_width - 13, 8, Color::LIME); Color::LIGHTGREEN } else { Color::WHITE }; if let Some(&byte) = byte { let top_text = format!("{}", byte); d.draw_text(&top_text, x + 2, 5, 20, color); } } } } fn draw_bottom_bar(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) { let height = d.get_screen_height(); let footer_top = (height - FOOTER_HEIGHT) as f32; // background d.draw_rectangle( 0, height - FOOTER_HEIGHT, d.get_screen_width(), FOOTER_HEIGHT, 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.; let bound_offset = 32. + gap * 2. + border * 2.; texture_option_button( d, Vector2 { x: 100. + col as f32 * bound_offset - if col < 0 { 10. } else { 0. }, y: footer_top + 5. + row as f32 * bound_offset, }, textures.get(texture), tool_option, &mut self.active_tool, 32., border, ); }; tool_button((0, -2), "eraser", Tool::Erase); tool_button((1, -2), "selection", Tool::SelectArea(None, false)); 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, 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, 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; let output_cells = (d.get_screen_width() - output_x) as usize / output_cell_width as usize; let y = footer_top as i32 + 5; if simple_button(d, output_x, y + 70, 65, 15) { self.output_as_text = !self.output_as_text } let output_mode_text = if self.output_as_text { "show bytes" } else { "show text" }; d.draw_text(output_mode_text, output_x + 5, y + 72, 10, Color::WHITE); let output_start = self.machine.output().len().saturating_sub(output_cells); let output_end = output_start + output_cells; for (box_index, index) in (output_start..output_end).enumerate() { let x = output_x + output_cell_width * box_index as i32; let expected_byte = self.level.outputs().get(index); let real_byte = self.machine.output().get(index); let (top_color, bottom_color) = if let (Some(&real_byte), Some(&expected_byte)) = (real_byte, expected_byte) { if expected_byte == real_byte { (Color::GREEN, Color::DARKGREEN) } else { (Color::RED, Color::DARKRED) } } else { (Color::DARKGRAY, Color::DIMGRAY) }; d.draw_rectangle(x, y, output_cell_width - 5, 30, top_color); d.draw_rectangle(x, y + 35, output_cell_width - 5, 30, bottom_color); if let Some(&expected_byte) = expected_byte { let top_text = if self.output_as_text && (expected_byte.is_ascii_graphic() || expected_byte.is_ascii_whitespace()) { format!("{:?}", expected_byte as char) } else { format!("{}", expected_byte) }; d.draw_text(&top_text, x + 2, y + 5, 20, Color::WHITE); } if let Some(&real_byte) = real_byte { let bottom_text = if self.output_as_text && (real_byte.is_ascii_graphic() || real_byte.is_ascii_whitespace()) { format!("{:?}", real_byte as char) } else { format!("{}", real_byte) }; d.draw_text(&bottom_text, x + 2, y + 40, 20, Color::WHITE); } } } fn board_overlay(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) { let footer_top = (d.get_screen_height() - FOOTER_HEIGHT) as f32; let mouse_pos = d.get_mouse_position(); let tile_scale = (1 << self.zoom) as f32; let tile_size = 16 << self.zoom; if self.sim_state == SimState::Editing { if let Tool::Digits(Some(pos)) = &mut self.active_tool { let tile_screen_pos = pos.to_vec() * tile_size as f32 + self.view_offset; d.draw_texture_ex( textures.get("selection"), tile_screen_pos, 0., tile_scale, Color::new(255, 180, 20, 255), ); if d.is_key_pressed(KeyboardKey::KEY_LEFT) { pos.x -= 1; } if d.is_key_pressed(KeyboardKey::KEY_RIGHT) { pos.x += 1; } if d.is_key_pressed(KeyboardKey::KEY_UP) { pos.y -= 1; } if d.is_key_pressed(KeyboardKey::KEY_DOWN) { pos.y += 1; } let pos = *pos; for n in 0..10 { if d.is_key_pressed(unsafe { transmute::(b'0' as u32 + n) }) { self.set_tile(pos, Tile::Digit(n as u8)); } } } if mouse_pos.y < footer_top && mouse_pos.y > HEADER_HEIGHT as f32 { let tile_pos = (mouse_pos - self.view_offset) / tile_size as f32; let tile_pos = Vector2::new(tile_pos.x.floor(), tile_pos.y.floor()); let tile_screen_pos = self.pos_to_screen(tile_pos); if self.active_tool != Tool::None { let tex = match self.active_tool { Tool::None => unreachable!(), Tool::Erase => "selection".into(), Tool::SetTile(t) => t.texture(), Tool::Math => format!("{}_off", self.tool_math.texture_name()), Tool::Gate => format!("{}_off", self.tool_gate.texture_name()), Tool::Wire => format!("{}_off", self.tool_wire.texture_name()), Tool::Arrow => self.tool_arrow.arrow_texture_name().into(), Tool::Mirror => self.tool_mirror.texture_name().into(), Tool::Digits(_) => "selection".into(), Tool::SelectArea(_, false) => "area_full".into(), Tool::SelectArea(_, true) => "transparent".into(), Tool::Blueprint => "transparent".into(), }; d.draw_texture_ex( textures.get(&tex), tile_screen_pos, 0., tile_scale, Color::new(255, 255, 255, 100), ); } if d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT) { let pos = tile_pos.into(); match self.active_tool { Tool::None => (), Tool::Erase => (), Tool::SetTile(tile) => self.set_tile(pos, tile), Tool::Math => { self.set_tile(pos, Tile::Powerable(PTile::Math(self.tool_math), false)) } Tool::Gate => { self.set_tile(pos, Tile::Powerable(PTile::Gate(self.tool_gate), false)) } Tool::Wire => { self.set_tile(pos, Tile::Powerable(PTile::Wire(self.tool_wire), false)) } Tool::Arrow => self.set_tile(pos, Tile::Arrow(self.tool_arrow)), Tool::Mirror => self.set_tile(pos, Tile::Mirror(self.tool_mirror)), Tool::Digits(_pos) => { self.active_tool = Tool::Digits(Some(pos)); if let Some(tile) = self.source_board.get_mut(pos) { if let Tile::Digit(_) = tile { } else { *tile = Tile::Digit(0); } } } 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_update_view(&mut pos); self.grow_board_and_update_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(_, _) => (), } } if d.is_mouse_button_down(MouseButton::MOUSE_BUTTON_LEFT) && self.active_tool == Tool::Erase { self.set_tile(tile_pos.into(), Tile::Blank) } if let Tool::SelectArea(selection, is_selecting) = &mut self.active_tool { if d.is_mouse_button_down(MouseButton::MOUSE_BUTTON_LEFT) { if *is_selecting { if let Some((_start, end)) = selection { *end = tile_pos.into(); } else { *selection = Some((tile_pos.into(), tile_pos.into())); } } else { *selection = Some((tile_pos.into(), tile_pos.into())); *is_selecting = true; } } else if d.is_mouse_button_released(MouseButton::MOUSE_BUTTON_LEFT) { *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 { let min = start.min(end); let max = start.max(end); let p_min = self.pos_to_screen(min.to_vec()); let p_max = self.pos_to_screen(max.to_vec()); let tex = textures.get("area_corner"); d.draw_texture_ex(tex, p_min, 0., tile_scale, Color::WHITE); let one_xy = Vector2::new(tile_size as f32, tile_size as f32); d.draw_texture_ex(tex, p_max + one_xy, 180., tile_scale, Color::WHITE); let top_right = Vector2::new(p_max.x + tile_size as f32, p_min.y); d.draw_texture_ex(tex, top_right, 90., tile_scale, Color::WHITE); let bot_left = Vector2::new(p_min.x, p_max.y + tile_size as f32); d.draw_texture_ex(tex, bot_left, -90., tile_scale, Color::WHITE); } } } } impl PartialEq for Tool { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::SetTile(l0), Self::SetTile(r0)) => l0 == r0, (Self::Digits(_), Self::Digits(_)) => true, (Self::SelectArea(_, _), Self::SelectArea(_, _)) => true, _ => ::core::mem::discriminant(self) == ::core::mem::discriminant(other), } } } 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 }