use std::{ fs::{read_dir, read_to_string}, mem::transmute, ops::Rem, time::Instant, }; use raylib::prelude::*; use crate::{ blueprint::Blueprint, level::Level, marble_engine::{board::*, pos::*, tile::*, Machine}, solution::*, theme::*, ui::*, util::*, TILE_TEXTURE_SIZE, }; const HEADER_HEIGHT: i32 = 40; const FOOTER_HEIGHT: i32 = 95; const SIDEBAR_WIDTH: i32 = 200 + 32 * 2 + 5 * 4; const END_POPUP_WIDTH: i32 = 320; const END_POPUP_HEIGHT: i32 = 165; const MAX_ZOOM: f32 = 8.; const MIN_ZOOM: f32 = 0.25; const BOARD_MARGIN: PosInt = 3; const MAX_SPEED_POWER: u8 = 16; const SPEED_DIGITS: i32 = 5; const MAX_FRAME_TIME_MICROS: u128 = 1_000_000 / 30; #[derive(Debug)] pub struct Editor { source_board: Board, level: Level, machine: Machine, sim_state: SimState, view_offset: Vector2, zoom: f32, output_as_text: bool, input_as_text: bool, active_tool: Tool, tool_math: MathOp, tool_comparator: Comparison, tool_arrow: Direction, tool_mirror: MirrorType, tool_wire: WireType, input_text_selected: bool, stage: Option, new_blueprint_name: String, blueprint_name_selected: bool, sim_speed: u8, time_since_step: f32, exit_state: ExitState, exit_menu: bool, total_steps: usize, popup: Popup, dismissed_end: bool, score: Option, tooltip: Tooltip, mouse: MouseInput, info_text: ShapedText, blueprints: Vec, selected_blueprint: usize, blueprint_scroll: usize, pasting_board: Option, /// draw grid, directions and values of marbles draw_overlay: bool, undo_history: Vec, undo_index: usize, // debug/profiling step_time: u128, max_step_time: u128, start_time: Instant, } #[derive(Debug, Clone)] enum Action { SetArea(ResizeDeltas, Pos, Board, Board), } #[derive(Debug, PartialEq, Clone, Copy)] enum Popup { None, Success, Failure, LevelInfo, PauseMenu, } #[derive(Debug, Clone)] enum Tool { None, Erase, SetTile(Tile), Digits(Option), Math, Comparator, Wire, Arrow, Mirror, SelectArea(Selection), Blueprint, } #[derive(Debug, Clone, Default)] struct Selection { area: Option<(Pos, Pos)>, is_selecting: bool, } #[derive(Debug, Clone, PartialEq)] enum SimState { Editing, Running, Stepping, } #[derive(Debug, Clone, Copy)] pub enum ExitState { Dont, ExitAndSave, Save, } impl Editor { pub fn new(solution: Solution, level: Level) -> Self { let mut output_as_text = true; let mut input_as_text = true; let mut stage = None; let mut machine = Machine::new_empty(); if let Some(i) = level.stages().first() { stage = Some(0); output_as_text = i.output().is_text(); input_as_text = i.input().is_text(); machine.set_input(i.input().as_bytes().to_owned()); } let mut info_text = ShapedText::new(20); info_text.set_text(level.description()); Self { source_board: Board::parse(&solution.board), machine, sim_state: SimState::Editing, view_offset: Vector2::zero(), zoom: 1., active_tool: Tool::None, output_as_text, input_as_text, input_text_selected: false, stage, new_blueprint_name: String::new(), blueprint_name_selected: false, sim_speed: 3, time_since_step: 0., tool_math: MathOp::Add, tool_comparator: Comparison::Equal, tool_arrow: Direction::Right, tool_mirror: MirrorType::Forward, tool_wire: WireType::Vertical, level, exit_state: ExitState::Dont, exit_menu: false, popup: Popup::None, dismissed_end: false, info_text, score: solution.score, total_steps: 0, blueprints: get_blueprints(), selected_blueprint: usize::MAX, blueprint_scroll: 0, step_time: 0, max_step_time: 0, start_time: Instant::now(), pasting_board: None, draw_overlay: true, undo_history: Vec::new(), undo_index: 0, tooltip: Tooltip::default(), mouse: MouseInput::default(), } } fn redo(&mut self) { if self.undo_index >= self.undo_history.len() { return; } let action = self.undo_history[self.undo_index].clone(); self.do_action(action); self.undo_index += 1; } fn push_action(&mut self, action: Action) { self.do_action(action.clone()); self.undo_history.truncate(self.undo_index); self.undo_history.push(action); self.undo_index += 1; } fn do_action(&mut self, action: Action) { match action { Action::SetArea(deltas, pos, _old, new) => { self.shift_world(deltas.x_neg as f32, deltas.y_neg as f32); self.source_board.grow(&deltas); self.source_board.paste_board(pos, &new); } } } fn undo(&mut self) { if self.undo_index == 0 { return; } self.undo_index -= 1; let action = &self.undo_history[self.undo_index]; match action { Action::SetArea(deltas, pos, old, _new) => { self.source_board.paste_board(*pos, old); self.source_board.shrink(deltas); self.shift_world(-(deltas.x_neg as f32), -(deltas.y_neg as f32)); } } } fn shift_world(&mut self, x: f32, y: f32) { match &mut self.active_tool { Tool::SelectArea(Selection { area: Some((a, b)), is_selecting: _, }) => { a.x += x as PosInt; a.y += y as PosInt; b.x += x as PosInt; b.y += y as PosInt; } Tool::Digits(Some(pos)) => { pos.x += x as PosInt; pos.y += y as PosInt; } _ => (), } let tile_size = TILE_TEXTURE_SIZE * self.zoom; self.view_offset.x -= x * tile_size; self.view_offset.y -= y * tile_size; } 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 * TILE_TEXTURE_SIZE * self.zoom + self.view_offset } fn init_sim(&mut self) { self.max_step_time = 0; self.total_steps = 0; self.start_time = Instant::now(); self.popup = Popup::None; self.dismissed_end = false; if !self.level.is_sandbox() { self.stage = Some(0); } self.reset_machine(); } fn reset_machine(&mut self) { self.machine.reset(); self.machine.set_board(self.source_board.clone()); if let Some(i) = self.stage { let bytes = self.level.stages()[i].input().as_bytes(); self.machine.set_input(bytes.to_owned()); } } fn step_pressed(&mut self) { match self.sim_state { SimState::Editing => { self.init_sim(); } SimState::Running => (), SimState::Stepping => self.step(), } self.sim_state = SimState::Stepping; } fn step(&mut self) { self.machine.step(); if let Some(i) = self.stage { if matches!(self.popup, Popup::Failure | Popup::Success) { self.popup = Popup::None; self.dismissed_end = true; } let stage = &self.level.stages()[i]; if self.popup == Popup::None && !self.dismissed_end { if stage.output().as_bytes() == self.machine.output() { if i + 1 < self.level.stages().len() { self.stage = Some(i + 1); self.total_steps += self.machine.step_count(); self.reset_machine(); } else { self.popup = Popup::Success; println!("completed in {:?}", self.start_time.elapsed()); self.exit_state = ExitState::Save; self.sim_state = SimState::Stepping; self.score = Some(Score { cycles: self.total_steps + self.machine.step_count(), tiles: self.source_board.count_tiles(), }); } } else if !stage.output().as_bytes().starts_with(self.machine.output()) { self.popup = Popup::Failure; self.sim_state = SimState::Stepping; } } } } fn rotate_tool(&mut self, shift: bool) { if shift { match &self.active_tool { Tool::Math => self.tool_math.prev(), Tool::Comparator => self.tool_comparator.prev(), Tool::Arrow => self.tool_arrow = self.tool_arrow.left(), Tool::Mirror => self.tool_mirror.flip(), Tool::Wire => self.tool_wire.prev(), _ => (), } } else { match &self.active_tool { Tool::Math => self.tool_math.next(), Tool::Comparator => self.tool_comparator.next(), Tool::Arrow => self.tool_arrow = self.tool_arrow.right(), Tool::Mirror => self.tool_mirror.flip(), Tool::Wire => self.tool_wire.next(), _ => (), } } } pub fn center_view(&mut self, d: &RaylibHandle) { let tile_size = TILE_TEXTURE_SIZE * self.zoom; 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: f32) { let tile_size = TILE_TEXTURE_SIZE * self.zoom; 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 = TILE_TEXTURE_SIZE * self.zoom; 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 { self.change_zoom_level(d, self.zoom); } } fn zoom_out(&mut self, d: &RaylibHandle) { if self.zoom > MIN_ZOOM { self.change_zoom_level(d, self.zoom / -2.); } } fn get_selected_as_board(&self, selection: (Pos, Pos)) -> Board { 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; self.source_board.get_rect(min, width, height) } fn save_blueprint(&mut self, selection: (Pos, Pos)) { let board = self.get_selected_as_board(selection); let id = get_free_id(&self.blueprints, Blueprint::id); let mut blueprint = Blueprint::new(&board, id); if !self.new_blueprint_name.is_empty() { blueprint.name.clone_from(&self.new_blueprint_name); } blueprint.save(); self.blueprints.push(blueprint); self.active_tool = Tool::Blueprint; } fn set_area(&mut self, pos: Pos, area: Board) { let old_area = self.source_board.get_rect(pos, area.width(), area.height()); if area == old_area { return; } let width = self.source_board.width() as PosInt; let height = self.source_board.height() as PosInt; let resize = ResizeDeltas { x_pos: if (pos.x + BOARD_MARGIN + area.width() as PosInt) > width { pos.x + BOARD_MARGIN + area.width() as PosInt - width } else { 0 } as usize, x_neg: if pos.x < BOARD_MARGIN { BOARD_MARGIN - pos.x } else { 0 } as usize, y_pos: if (pos.y + BOARD_MARGIN + area.height() as PosInt) > height { pos.y + BOARD_MARGIN + area.height() as PosInt - height } else { 0 } as usize, y_neg: if pos.y < BOARD_MARGIN { BOARD_MARGIN - pos.y } else { 0 } as usize, }; let mut pos = pos; pos.x += resize.x_neg as PosInt; pos.y += resize.y_neg as PosInt; self.push_action(Action::SetArea(resize, pos, old_area, area)); } fn set_tile(&mut self, pos: Pos, tile: Tile) { self.set_area(pos, Board::new_single(tile)); } pub fn update(&mut self, rl: &RaylibHandle) { self.tooltip.init_frame(rl); self.mouse = MouseInput::get(rl); if self.popup != Popup::None { self.mouse.clear(); } if rl.is_key_pressed(KeyboardKey::KEY_ESCAPE) { self.popup = match self.popup { Popup::Success | Popup::Failure => { self.dismissed_end = true; Popup::None } Popup::None => Popup::PauseMenu, _ => Popup::None, }; } if self.sim_state == SimState::Running && self.popup == Popup::None { self.time_since_step += rl.get_frame_time(); let step_size = 1. / (1 << self.sim_speed) as f32; let mut steps_taken = 0; let start_time = Instant::now(); if self.time_since_step > step_size { let step_count = (self.time_since_step / step_size) as u32; for _ in 0..step_count { if self.sim_state != SimState::Running { // pause on level completion break; } if start_time.elapsed().as_micros() > MAX_FRAME_TIME_MICROS { break; } self.step(); steps_taken += 1; } self.time_since_step -= step_count as f32 * step_size; } let avg_step_time = start_time .elapsed() .as_micros() .checked_div(steps_taken) .unwrap_or_default(); self.step_time = avg_step_time; self.max_step_time = avg_step_time.max(self.max_step_time); } 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.init_sim(); self.sim_state = SimState::Running; } SimState::Running => { self.sim_state = SimState::Editing; self.popup = Popup::None; } SimState::Stepping => self.sim_state = SimState::Running, } } let mouse_pos = self.mouse.pos(); if (self.active_tool != Tool::Blueprint || self.sim_state != SimState::Editing || mouse_pos.x > SIDEBAR_WIDTH as f32) && mouse_pos.y > HEADER_HEIGHT as f32 && (mouse_pos.y as i32) < (rl.get_screen_height() - FOOTER_HEIGHT) { match self.mouse.scroll() { Some(Scroll::Up) => self.zoom_in(rl), Some(Scroll::Down) => self.zoom_out(rl), None => (), } } if self.mouse.right_hold() { let speed = if rl.is_key_down(KeyboardKey::KEY_LEFT_SHIFT) { 4. } else { 1. }; self.view_offset += rl.get_mouse_delta() * speed; } if rl.is_key_pressed(KeyboardKey::KEY_HOME) { self.center_view(rl); } if rl.is_key_pressed(KeyboardKey::KEY_R) { self.rotate_tool(rl.is_key_down(KeyboardKey::KEY_LEFT_SHIFT)); } if self.sim_state == SimState::Editing { if rl.is_key_down(KeyboardKey::KEY_LEFT_CONTROL) { if rl.is_key_pressed(KeyboardKey::KEY_V) { if let Ok(text) = rl.get_clipboard_text() { let b = Board::parse(&text); self.pasting_board = Some(b); } } else if rl.is_key_pressed(KeyboardKey::KEY_Z) { self.undo() } else if rl.is_key_pressed(KeyboardKey::KEY_Y) { self.redo(); } } } } 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); if self.draw_overlay { self.machine .draw_marble_values(d, textures, self.view_offset, self.zoom); } } } pub fn draw(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) { d.clear_background(BG_WORLD); if self.draw_overlay && self.zoom >= 0.5 { let tile_size = TILE_TEXTURE_SIZE * self.zoom; 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; for y in 0..=(d.get_screen_height() / tile_size as i32) { let y = y * tile_size as i32 + grid_spill_y as i32; d.draw_line(0, y, d.get_screen_width(), y, FG_GRID); } for x in 0..=(d.get_screen_width() / tile_size as i32) { let x = x * tile_size as i32 + grid_spill_x as i32; d.draw_line(x, 0, x, d.get_screen_height(), FG_GRID); } } 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 { self.draw_blueprint_sidebar(d, textures); } self.mouse = MouseInput::get(d); if self.popup != Popup::None { self.tooltip.reset(); d.draw_rectangle( 0, 0, d.get_screen_width(), d.get_screen_height(), Color::new(100, 100, 100, 120), ); } match self.popup { Popup::Success | Popup::Failure => { self.draw_end_popup(d, textures); } Popup::LevelInfo => { let bounds = screen_centered_rect_dyn(d, 100, 100); d.draw_rectangle_rec(bounds, BG_MEDIUM); d.draw_text(self.level.name(), 110, 110, 30, Color::LIGHTBLUE); self.info_text.update_width(d, bounds.width as i32 - 20); self.info_text.draw(d, 110, 140); } Popup::PauseMenu => { let bounds = screen_centered_rect(d, 300, 400); d.draw_rectangle_rec(bounds, BG_MEDIUM); let x = bounds.x as i32; let y = bounds.y as i32; d.draw_text("Menu", x + 10, y + 10, 30, Color::LIGHTBLUE); if text_button(d, &self.mouse, x + 10, y + 40, 280, "Resume") { self.popup = Popup::None; } if text_button(d, &self.mouse, x + 10, y + 80, 280, "Save") { self.exit_state = ExitState::Save; self.popup = Popup::None; } if text_button(d, &self.mouse, x + 10, y + 120, 280, "Exit to main menu") { self.exit_state = ExitState::ExitAndSave; } } Popup::None => (), } self.tooltip.draw(d); } fn draw_blueprint_sidebar(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) { 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; let blueprints_shown = (sidebar_height as usize - 45) / 37; let end = self .blueprints .len() .min(blueprints_shown + self.blueprint_scroll); for (i, b) in self.blueprints[self.blueprint_scroll..end] .iter_mut() .enumerate() { let i = i + self.blueprint_scroll; if tex32_button( (d, &self.mouse), (5, y), textures.get("rubbish"), (&mut self.tooltip, "Delete"), ) { b.remove_file(); self.blueprints.remove(i); break; } let is_selected = self.selected_blueprint == i; let mut text_selected = is_selected && self.blueprint_name_selected; text_input( d, &self.mouse, 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; } self.tooltip.add(42 + 205, y, 32, 32, "Select"); simple_option_button( (d, &self.mouse), rect(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 }), ); y += 37; } } fn draw_end_popup(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) { let bounds = screen_centered_rect(d, END_POPUP_WIDTH, END_POPUP_HEIGHT); let x = bounds.x as i32; let y = bounds.y as i32; d.draw_rectangle_rec(bounds, BG_DARK); if self.popup == Popup::Success { d.draw_text("Level Complete!", x + 45, y + 10, 30, Color::LIME); if let Some(score) = &self.score { d.draw_text("cycles", x + 15, y + 45, 20, Color::WHITE); draw_usize(d, textures, score.cycles, (x + 10, y + 70), 9, 2); d.draw_text("tiles", x + 215, y + 45, 20, Color::WHITE); draw_usize(d, textures, score.tiles, (x + 210, y + 70), 5, 2); } if simple_button((d, &self.mouse), x + 10, y + 110, 140, 45) { self.popup = Popup::None; self.dismissed_end = true; } d.draw_text("continue\nediting", x + 15, y + 115, 20, Color::WHITE); if simple_button( (d, &self.mouse), x + END_POPUP_WIDTH / 2 + 5, y + 110, 140, 45, ) { self.exit_state = ExitState::ExitAndSave; } d.draw_text( "return to\nlevel list", x + END_POPUP_WIDTH / 2 + 10, y + 115, 20, Color::WHITE, ); } else { d.draw_text("Level Failed!", x + 45, y + 10, 30, Color::RED); d.draw_text("incorrect output", x + 45, y + 45, 20, Color::WHITE); if simple_button((d, &self.mouse), x + 10, y + 110, 300, 25) { self.popup = Popup::None; self.dismissed_end = true; } d.draw_text("ok :(", x + 15, 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 tex32_button( (d, &self.mouse), (4, 4), textures.get("exit"), (&mut self.tooltip, "exit"), ) { if self.exit_menu { self.exit_state = ExitState::ExitAndSave; } else { self.exit_menu = true; } } if self.exit_menu { if tex32_button( (d, &self.mouse), (40, 4), textures.get("cancel"), (&mut self.tooltip, "cancel"), ) { self.exit_menu = false; } } else if tex32_button( (d, &self.mouse), (40, 4), textures.get("save"), (&mut self.tooltip, "save"), ) { self.exit_state = ExitState::Save; } if text_button(d, &self.mouse, 76, 5, 50, "info") { self.popup = Popup::LevelInfo; } if self.sim_state == SimState::Editing { let undo_icon = if self.undo_index > 0 { "undo" } else { "undo_disabled" }; if tex32_button( (d, &self.mouse), (150, 4), textures.get(undo_icon), (&mut self.tooltip, "Undo"), ) { self.undo() } let redo_icon = if self.undo_index < self.undo_history.len() { "redo" } else { "redo_disabled" }; if tex32_button( (d, &self.mouse), (186, 4), textures.get(redo_icon), (&mut self.tooltip, "Redo"), ) { self.redo() } } let overlay_btn_icon = if self.draw_overlay { "marble_overlay" } else { "marble" }; if tex32_button( (d, &self.mouse), (223, 4), textures.get(overlay_btn_icon), (&mut self.tooltip, "Toggle overlay"), ) { self.draw_overlay = !self.draw_overlay; } if self.sim_state == SimState::Running { if tex32_button( (d, &self.mouse), (260, 4), textures.get("pause"), (&mut self.tooltip, "Pause"), ) { self.sim_state = SimState::Stepping; } } else if tex32_button( (d, &self.mouse), (260, 4), textures.get("play"), (&mut self.tooltip, "Start"), ) { if self.sim_state == SimState::Editing { self.init_sim() } self.sim_state = SimState::Running; } if self.sim_state != SimState::Editing { if tex32_button( (d, &self.mouse), (296, 4), textures.get("stop"), (&mut self.tooltip, "Stop"), ) { self.sim_state = SimState::Editing; self.popup = Popup::None; } } if tex32_button( (d, &self.mouse), (332, 4), textures.get("step"), (&mut self.tooltip, "Step"), ) { self.step_pressed(); } self.tooltip.add(368, 4, 48, 32, "Speed"); draw_usize(d, textures, 1 << self.sim_speed, (368, 4), SPEED_DIGITS, 1); slider( d, &self.mouse, &mut self.sim_speed, 0, MAX_SPEED_POWER, 368, 24, 48, 12, ); self.tooltip.add(420, 4, 180, 32, "Steps"); draw_usize(d, textures, self.machine.step_count(), (420, 4), 9, 2); if self.stage > Some(0) { self.tooltip.add(420, 44, 180, 32, "Total steps"); let total_steps = self.total_steps + self.machine.step_count(); draw_usize(d, textures, total_steps, (420, 44), 9, 2); } draw_usize(d, textures, self.step_time as usize, (260, 42), 9, 1); draw_usize(d, textures, self.max_step_time as usize, (260, 60), 9, 1); d.draw_text("input:", 603, 8, 10, Color::WHITE); if simple_button((d, &self.mouse), 600, 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, 603, 23, 10, Color::WHITE); let input_x = 638; 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, &self.mouse, 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, BG_WIDGET); 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(Selection { area: Some(selection), is_selecting: _, }) = self.active_tool { hide_tile_tools = true; text_input( d, &self.mouse, Rectangle::new(100., footer_top + 10., 240., 30.), &mut self.new_blueprint_name, &mut self.blueprint_name_selected, 32, true, ); let y = footer_top as i32 + 49; self.tooltip.add(100, y, 40, 40, "Cancel"); if simple_button((d, &self.mouse), 100, y, 40, 40) || d.is_key_pressed(KeyboardKey::KEY_ESCAPE) { self.active_tool = Tool::SelectArea(Selection::default()); } draw_scaled_texture(d, textures.get("cancel"), 104, y + 4, 2.); self.tooltip.add(144, y, 40, 40, "Save blueprint"); if simple_button((d, &self.mouse), 144, y, 40, 40) { self.save_blueprint(selection); } draw_scaled_texture(d, textures.get("save"), 148, y + 4, 2.); self.tooltip.add(188, y, 40, 40, "Copy"); if simple_button((d, &self.mouse), 188, y, 40, 40) || (d.is_key_pressed(KeyboardKey::KEY_C) && d.is_key_down(KeyboardKey::KEY_LEFT_CONTROL)) { let board = self.get_selected_as_board(selection); self.pasting_board = Some(board); } draw_scaled_texture(d, textures.get("copy"), 192, y + 4, 2.); self.tooltip.add(232, y, 40, 40, "Delete"); if simple_button((d, &self.mouse), 232, y, 40, 40) { let min = selection.0.min(selection.1); let max = selection.0.max(selection.1); let board = Board::new_empty((max.x - min.x) as usize + 1, (max.y - min.y) as usize + 1); self.set_area(min, board); } draw_scaled_texture(d, textures.get("eraser"), 236, y + 4, 2.); } let mut tool_button = |(row, col): (i32, i32), texture: &str, tooltip: &'static str, tool_option: Tool| { let border = 4.; let gap = 2.; let tex_size = 32.; let button_size = tex_size + border * 2.; let grid_size = button_size + gap * 2.; let pos = Vector2 { x: 100. + col as f32 * grid_size - if col < 0 { 10. } else { 0. }, y: footer_top + 5. + row as f32 * grid_size, }; self.tooltip.add_rec( Rectangle::new(pos.x, pos.y, button_size, button_size), tooltip, ); scrollable_texture_option_button( d, &self.mouse, pos, textures.get(texture), tool_option, &mut self.active_tool, tex_size, border, ) }; tool_button((0, -2), "eraser", "Eraser", Tool::Erase); tool_button( (1, -2), "selection", "Select", Tool::SelectArea(Selection::default()), ); tool_button((0, -1), "blueprint", "Blueprints", Tool::Blueprint); tool_button((1, -1), "transparent", "None", Tool::None); if !hide_tile_tools { tool_button( (0, 0), "block", "Block", Tool::SetTile(Tile::from_char('#')), ); tool_button( (0, 1), "silo_off", "Silo", Tool::SetTile(Tile::from_char('B')), ); tool_button( (0, 2), "button_off", "Button", Tool::SetTile(Tile::from_char('*')), ); tool_button( (0, 3), "io_tile_off", "Input/Output silo", Tool::SetTile(Tile::from_char('I')), ); tool_button( (0, 4), "flipper_off", "Flipper", Tool::SetTile(Tile::from_char('F')), ); tool_button((0, 5), "digit_tool", "Digit", Tool::Digits(None)); tool_button( (1, 0), "marble", "Marble", Tool::SetTile(Tile::from_char('o')), ); match tool_button( (1, 1), self.tool_wire.texture_name_off(), self.tool_wire.human_name(), Tool::Wire, ) { Some(Scroll::Down) => self.tool_wire.next(), Some(Scroll::Up) => self.tool_wire.prev(), None => (), } match tool_button( (1, 2), self.tool_arrow.arrow_tile_texture_name(), self.tool_arrow.arrow_tile_human_name(), Tool::Arrow, ) { Some(Scroll::Down) => self.tool_arrow = self.tool_arrow.right(), Some(Scroll::Up) => self.tool_arrow = self.tool_arrow.left(), None => (), } if tool_button( (1, 3), self.tool_mirror.texture_name(), self.tool_mirror.human_name(), Tool::Mirror, ) .is_some() { self.tool_mirror.flip() } match tool_button( (1, 4), self.tool_math.texture_name_off(), self.tool_math.human_name(), Tool::Math, ) { Some(Scroll::Down) => self.tool_math.next(), Some(Scroll::Up) => self.tool_math.prev(), None => (), } match tool_button( (1, 5), self.tool_comparator.texture_name_off(), self.tool_comparator.human_name(), Tool::Comparator, ) { Some(Scroll::Down) => self.tool_comparator.next(), Some(Scroll::Up) => self.tool_comparator.prev(), None => (), } } let y = footer_top as i32 + 5; if let Some(i) = self.stage { d.draw_text("stage", 370, y, 20, Color::GREEN); let shown_stage = if self.sim_state == SimState::Editing { 0 } else { i + 1 }; let text = format!("{shown_stage}/{}", self.level.stages().len()); d.draw_text(&text, 370, y + 20, 20, Color::LIGHTGREEN); } let output_x = 440; let output_cell_width = 43; let output_cells = (d.get_screen_width() - output_x) as usize / output_cell_width as usize; if simple_button((d, &self.mouse), 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 (mut top_color, mut bottom_color) = (BG_LIGHT, BG_MEDIUM); let real_byte = self.machine.output().get(index); if let Some(stage_index) = self.stage { let stage = &self.level.stages()[stage_index]; let expected_byte = stage.output().as_bytes().get(index); if let (Some(&real_byte), Some(&expected_byte)) = (real_byte, expected_byte) { (top_color, bottom_color) = if expected_byte == real_byte { (Color::GREEN, Color::DARKGREEN) } else { (Color::RED, Color::DARKRED) }; } d.draw_rectangle(x, y, output_cell_width - 5, 30, top_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); } } d.draw_rectangle(x, y + 35, output_cell_width - 5, 30, bottom_color); 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 tile_size = TILE_TEXTURE_SIZE * self.zoom; if self.sim_state == SimState::Editing { if let Some(board) = &self.pasting_board { if d.is_key_pressed(KeyboardKey::KEY_ESCAPE) { self.pasting_board = None; return; } if self.mouse.pos().y < footer_top && self.mouse.pos().y > HEADER_HEIGHT as f32 { let view_offset = Vector2::new( self.view_offset.x.rem(tile_size), self.view_offset.y.rem(tile_size), ); let mut offset = self.mouse.pos() - view_offset; offset.x -= offset.x.rem(tile_size); offset.y -= offset.y.rem(tile_size); offset += view_offset; board.draw(d, textures, offset, self.zoom); if self.mouse.left_click() { let tile_pos = (self.mouse.pos() - self.view_offset) / tile_size; let tile_pos = Vector2::new(tile_pos.x.floor(), tile_pos.y.floor()); let board = self.pasting_board.take().unwrap(); self.set_area(tile_pos.into(), board); } } return; } if let Tool::Digits(Some(pos)) = &mut self.active_tool { let tile_screen_pos = pos.to_vec() * tile_size + self.view_offset; d.draw_texture_ex( textures.get("selection"), tile_screen_pos, 0., self.zoom, 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::Open(OpenTile::Digit(n as u8), Claim::Free)); } } } if self.mouse.pos().y < footer_top && self.mouse.pos().y > HEADER_HEIGHT as f32 { let tile_pos = (self.mouse.pos() - self.view_offset) / tile_size; 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 => "cancel", Tool::SetTile(t) => t.texture(), Tool::Math => self.tool_math.texture_name_off(), Tool::Comparator => self.tool_comparator.texture_name_off(), Tool::Wire => self.tool_wire.texture_name_off(), Tool::Arrow => self.tool_arrow.arrow_tile_texture_name(), Tool::Mirror => self.tool_mirror.texture_name(), Tool::Digits(_) => "selection", Tool::SelectArea(selection) => { if selection.is_selecting { "transparent" } else { "area_full" } } Tool::Blueprint => "transparent", }; d.draw_texture_ex( textures.get(tex), tile_screen_pos, 0., self.zoom, Color::new(255, 255, 255, 100), ); } if self.mouse.left_click() { let pos = tile_pos.into(); match self.active_tool { Tool::None | Tool::Erase | Tool::SelectArea(_) => (), Tool::SetTile(tile) => self.set_tile(pos, tile), Tool::Math => { self.set_tile(pos, Tile::Powerable(PTile::Math(self.tool_math), false)); } Tool::Comparator => self.set_tile( pos, Tile::Powerable(PTile::Comparator(self.tool_comparator), false), ), Tool::Wire => self.set_tile(pos, Tile::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)); let tile = self.source_board.get_or_blank(pos); if !matches!(tile, Tile::Open(OpenTile::Digit(_), _)) { self.set_tile(pos, Tile::Open(OpenTile::Digit(0), Claim::Free)); } } Tool::Blueprint => { if self.mouse.pos().x > SIDEBAR_WIDTH as f32 { if let Some(bp) = self.blueprints.get(self.selected_blueprint) { let board = bp.get_board().unwrap().clone(); self.set_area(pos, board); } } } } } if self.mouse.left_hold() && self.active_tool == Tool::Erase { self.set_tile(tile_pos.into(), Tile::BLANK); } if let Tool::SelectArea(selection) = &mut self.active_tool { if self.mouse.left_hold() { if selection.is_selecting { if let Some((_start, end)) = &mut selection.area { *end = tile_pos.into(); } else { selection.area = Some((tile_pos.into(), tile_pos.into())); } } else { selection.area = Some((tile_pos.into(), tile_pos.into())); selection.is_selecting = true; } } else if self.mouse.left_release() { selection.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), self.view_offset.y.rem(tile_size), ); let mut offset = self.mouse.pos() - view_offset; offset.x -= offset.x.rem(tile_size); offset.y -= offset.y.rem(tile_size); offset += view_offset; bp.convert_board().draw(d, textures, offset, self.zoom); } if self.mouse.pos().x < SIDEBAR_WIDTH as f32 { if self.mouse.scroll() == Some(Scroll::Down) && self.blueprint_scroll < self.blueprints.len().saturating_sub(5) { self.blueprint_scroll += 1; } else if self.mouse.scroll() == Some(Scroll::Up) && self.blueprint_scroll > 0 { self.blueprint_scroll -= 1; } } } } // draw selection if let Tool::SelectArea(Selection { area: Some((start, end)), is_selecting: _, }) = self.active_tool { let p_min = self.pos_to_screen(start.min(end).to_vec()); let p_max = self.pos_to_screen((start.max(end) + (1, 1).into()).to_vec()); let x = p_min.x as i32; let y = p_min.y as i32; let width = p_max.x as i32 - x; let height = p_max.y as i32 - y; d.draw_rectangle(x, y, width, 4, Color::WHITE); d.draw_rectangle(x, y + height - 4, width, 4, Color::WHITE); d.draw_rectangle(x, y, 4, height, Color::WHITE); d.draw_rectangle(x + width - 4, y, 4, height, 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 }