enter and exit solution editor

This commit is contained in:
Crispy 2024-10-06 16:00:12 +02:00
parent 44b7d63cde
commit c4381ac1a1
8 changed files with 195 additions and 41 deletions

View file

@ -4,9 +4,8 @@
logic mostly like https://git.crispypin.cc/CrispyPin/marble logic mostly like https://git.crispypin.cc/CrispyPin/marble
## todo ## todo
level selection
solution saving & loading solution saving & loading
cleanup: unpowered texture names x_off -> x
input/output display input/output display
grow grid automatically while editing grow grid automatically while editing
sim/speed control gui sim/speed control gui

View file

@ -9,9 +9,14 @@ use crate::{
tile::{Direction, GateType, MathOp, MirrorType, PTile, Tile, WireType}, tile::{Direction, GateType, MathOp, MirrorType, PTile, Tile, WireType},
Machine, Machine,
}, },
simple_button,
solution::Solution,
text_input, texture_option_button, Textures, text_input, texture_option_button, Textures,
}; };
const HEADER_HEIGHT: i32 = 40;
const FOOTER_HEIGHT: i32 = 95;
#[derive(Debug)] #[derive(Debug)]
pub struct Editor { pub struct Editor {
source_board: Board, source_board: Board,
@ -31,6 +36,8 @@ pub struct Editor {
input_text_selected: bool, input_text_selected: bool,
sim_speed: f32, sim_speed: f32,
time_since_step: f32, time_since_step: f32,
exit_state: ExitState,
exit_menu: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -53,10 +60,17 @@ enum SimState {
Stepping, Stepping,
} }
#[derive(Debug, Clone, Copy)]
pub enum ExitState {
Dont,
ExitAndSave,
ExitNoSave,
}
impl Editor { impl Editor {
pub fn new_sandbox() -> Self { pub fn new(solution: Solution, level: Level) -> Self {
Self { Self {
source_board: Board::new_empty(16, 16), source_board: Board::parse(&solution.board),
machine: Machine::new_empty(1), machine: Machine::new_empty(1),
sim_state: SimState::Editing, sim_state: SimState::Editing,
view_offset: Vector2::zero(), view_offset: Vector2::zero(),
@ -72,10 +86,28 @@ impl Editor {
tool_menu_arrow: Direction::Right, tool_menu_arrow: Direction::Right,
tool_menu_mirror: MirrorType::Forward, tool_menu_mirror: MirrorType::Forward,
tool_menu_wire: WireType::Vertical, tool_menu_wire: WireType::Vertical,
level: Level::new_sandbox(), level,
exit_state: ExitState::Dont,
exit_menu: false,
} }
} }
pub fn get_exit_state(&self) -> ExitState {
self.exit_state
}
pub fn level_id(&self) -> &str {
&self.level.id()
}
pub fn save(&mut self) {
todo!()
}
pub fn source_board(&self) -> &Board {
&self.source_board
}
pub fn load_board(&mut self, board: Board) { pub fn load_board(&mut self, board: Board) {
self.source_board = board; self.source_board = board;
} }
@ -131,6 +163,9 @@ impl Editor {
} }
pub fn input(&mut self, rl: &RaylibHandle) { pub fn input(&mut self, rl: &RaylibHandle) {
if rl.is_key_pressed(KeyboardKey::KEY_ESCAPE) {
self.sim_state = SimState::Editing
}
if self.sim_state == SimState::Running { if self.sim_state == SimState::Running {
self.time_since_step += rl.get_frame_time(); self.time_since_step += rl.get_frame_time();
if self.time_since_step > 1. / self.sim_speed { if self.time_since_step > 1. / self.sim_speed {
@ -149,9 +184,6 @@ impl Editor {
} }
self.sim_state = SimState::Stepping; self.sim_state = SimState::Stepping;
} }
if rl.is_key_pressed(KeyboardKey::KEY_ESCAPE) {
self.sim_state = SimState::Editing;
}
if rl.is_key_pressed(KeyboardKey::KEY_ENTER) { if rl.is_key_pressed(KeyboardKey::KEY_ENTER) {
match self.sim_state { match self.sim_state {
SimState::Editing => { SimState::Editing => {
@ -175,6 +207,10 @@ impl Editor {
if rl.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_RIGHT) { if rl.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_RIGHT) {
self.view_offset = Vector2::zero(); self.view_offset = Vector2::zero();
} }
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) { fn draw_board(&self, d: &mut RaylibDrawHandle, textures: &Textures) {
@ -192,18 +228,6 @@ impl Editor {
pub fn draw(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) { pub fn draw(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) {
d.clear_background(Color::new(64, 64, 64, 255)); d.clear_background(Color::new(64, 64, 64, 255));
self.draw_board(d, textures);
let height = d.get_screen_height();
let footer_height = 95;
let footer_top = (height - footer_height) as f32;
d.draw_rectangle(
0,
height - footer_height,
d.get_screen_width(),
footer_height,
Color::new(32, 32, 32, 255),
);
let tile_size = (16 << self.zoom) as f32; let tile_size = (16 << self.zoom) as f32;
let grid_spill_x = (self.view_offset.x).rem(tile_size) - tile_size; let grid_spill_x = (self.view_offset.x).rem(tile_size) - tile_size;
@ -213,13 +237,62 @@ impl Editor {
grid_spill_x, grid_spill_x,
grid_spill_y, grid_spill_y,
d.get_screen_width() as f32 * 2., d.get_screen_width() as f32 * 2.,
height as f32 - grid_spill_y - footer_height as f32, d.get_screen_height() as f32 * 2.,
), ),
None, None,
tile_size, tile_size,
1, 1,
); );
self.draw_board(d, textures);
self.board_overlay(d, textures);
self.draw_bottom_bar(d, textures);
self.draw_top_bar(d, textures);
}
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, 80, 30) {
self.exit_menu = false;
}
d.draw_text("cancel", 10, 10, 20, Color::WHITE);
if simple_button(d, 90, 5, 80, 30) {
self.exit_state = ExitState::ExitAndSave;
}
d.draw_text("save", 95, 10, 20, Color::WHITE);
if simple_button(d, 175, 5, 80, 30) {
self.exit_state = ExitState::ExitNoSave;
}
d.draw_text("revert", 180, 10, 20, Color::WHITE);
} else {
if simple_button(d, 5, 5, 80, 30) {
self.exit_menu = true;
}
d.draw_text("exit", 10, 10, 20, Color::WHITE);
}
}
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),
);
d.gui_check_box( d.gui_check_box(
Rectangle::new(5., footer_top + 5., 25., 25.), Rectangle::new(5., footer_top + 5., 25., 25.),
Some(rstr!("output as text")), Some(rstr!("output as text")),
@ -297,12 +370,10 @@ impl Editor {
&Tile::Powerable(PTile::Gate(self.tool_menu_gate), false).texture(), &Tile::Powerable(PTile::Gate(self.tool_menu_gate), false).texture(),
Tool::Gate, Tool::Gate,
); );
}
let is_shift = d.is_key_down(KeyboardKey::KEY_LEFT_SHIFT); fn board_overlay(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) {
if d.is_key_pressed(KeyboardKey::KEY_R) { let footer_top = (d.get_screen_height() - FOOTER_HEIGHT) as f32;
self.rotate_tool(is_shift);
}
let mouse_pos = d.get_mouse_position(); let mouse_pos = d.get_mouse_position();
if self.sim_state == SimState::Editing { if self.sim_state == SimState::Editing {
if let Tool::Digits(Some(pos)) = &mut self.active_tool { if let Tool::Digits(Some(pos)) = &mut self.active_tool {
@ -334,7 +405,7 @@ impl Editor {
pos.y += 1; pos.y += 1;
} }
} }
if mouse_pos.y < footer_top { if mouse_pos.y < footer_top && mouse_pos.y > HEADER_HEIGHT as f32 {
let tile_pos = (mouse_pos - self.view_offset) / (16 << self.zoom) as f32; let tile_pos = (mouse_pos - self.view_offset) / (16 << self.zoom) as f32;
let tile_pos = Vector2::new(tile_pos.x.floor(), tile_pos.y.floor()); let tile_pos = Vector2::new(tile_pos.x.floor(), tile_pos.y.floor());

View file

@ -2,7 +2,7 @@ use serde::Deserialize;
use crate::marble_engine::board::Board; use crate::marble_engine::board::Board;
#[derive(Debug, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Level { pub struct Level {
id: String, id: String,
name: String, name: String,
@ -36,8 +36,12 @@ impl Level {
&self.description &self.description
} }
pub fn init_board(&self) -> Option<Board> { // pub fn init_board(&self) -> Option<Board> {
self.init_board.as_deref().map(Board::parse) // self.init_board.as_deref().map(Board::parse)
// }
pub fn init_board(&self)->Option<String>{
self.init_board.clone()
} }
pub fn inputs(&self) -> &[u8] { pub fn inputs(&self) -> &[u8] {

View file

@ -11,7 +11,7 @@ mod marble_engine;
mod solution; mod solution;
mod util; mod util;
use editor::Editor; use editor::{Editor, ExitState};
use level::Level; use level::Level;
use marble_engine::board::Board; use marble_engine::board::Board;
use solution::Solution; use solution::Solution;
@ -67,6 +67,16 @@ impl Game {
if let Some(editor) = &mut self.open_editor { if let Some(editor) = &mut self.open_editor {
editor.input(&d); editor.input(&d);
editor.draw(&mut d, &self.textures); editor.draw(&mut d, &self.textures);
match editor.get_exit_state() {
ExitState::Dont => (),
ExitState::ExitNoSave => self.open_editor = None,
ExitState::ExitAndSave => {
self.solutions.get_mut(editor.level_id()).unwrap()
[self.selected_solution]
.board = editor.source_board().to_string();
self.open_editor = None;
}
}
} else { } else {
self.draw(&mut d); self.draw(&mut d);
} }
@ -165,7 +175,7 @@ impl Game {
if simple_button(d, level_list_width + 10, y, solution_entry_width, 30) { if simple_button(d, level_list_width + 10, y, solution_entry_width, 30) {
let n = solutions.len(); let n = solutions.len();
solutions.push(Solution::new(level.id().to_owned(), n)); solutions.push(Solution::new(&level, n));
} }
d.draw_text( d.draw_text(
"new solution", "new solution",
@ -179,7 +189,7 @@ impl Game {
let bounds = Rectangle { let bounds = Rectangle {
x: (level_list_width + 10 + solution_entry_width + 10) as f32, x: (level_list_width + 10 + solution_entry_width + 10) as f32,
y: 60., y: 60.,
width: 240., width: 220.,
height: 30., height: 30.,
}; };
text_input( text_input(
@ -188,6 +198,12 @@ impl Game {
&mut solution.name, &mut solution.name,
&mut self.editing_solution_name, &mut self.editing_solution_name,
); );
let button_x = level_list_width + solution_entry_width + 20;
if simple_button(d, button_x, 100, 220, 30) {
self.open_editor = Some(Editor::new(solution.clone(), level.clone()));
}
d.draw_text("edit", button_x + 5, 105, 20, Color::WHITE);
} }
} else { } else {
self.solutions.insert(level.id().to_owned(), Vec::new()); self.solutions.insert(level.id().to_owned(), Vec::new());

View file

@ -63,6 +63,17 @@ impl Board {
Board::new(rows) Board::new(rows)
} }
pub fn to_string(&self)->String{
let mut out = String::new();
for row in &self.rows{
for tile in row{
out.push(tile.to_char())
}
out.push('\n');
}
out
}
pub fn new_empty(width: usize, height: usize) -> Self { pub fn new_empty(width: usize, height: usize) -> Self {
let rows = vec![vec![Tile::Blank; width]; height]; let rows = vec![vec![Tile::Blank; width]; height];
Self { Self {

View file

@ -104,6 +104,50 @@ impl Tile {
} }
} }
pub fn to_char(self) -> char {
match self {
Tile::Blank => ' ',
Tile::Block => '#',
Tile::Marble { value: _, dir: _ } => 'o',
Tile::Digit(n) => (b'0' + n) as char,
Tile::Mirror(dir) => match dir {
MirrorType::Forward => '/',
MirrorType::Back => '\\',
},
Tile::Arrow(dir) => match dir {
Direction::Up => '^',
Direction::Down => 'v',
Direction::Left => '<',
Direction::Right => '>',
},
Tile::Powerable(tile, _) => match tile {
PTile::Trigger => '*',
PTile::Wire(wire) => match wire {
WireType::Vertical => '|',
WireType::Horizontal => '-',
WireType::Cross => '+',
},
PTile::Gate(gate) => match gate {
GateType::LessThan => 'L',
GateType::GreaterThan => 'G',
GateType::Equal => '=',
GateType::NotEqual => '!',
},
PTile::Math(math) => match math {
MathOp::Add => 'A',
MathOp::Sub => 'S',
MathOp::Mul => 'M',
MathOp::Div => 'D',
MathOp::Rem => 'R',
},
PTile::Bag => 'B',
PTile::Flipper => 'F',
PTile::Input => 'I',
PTile::Output => 'P',
},
}
}
pub fn is_blank(&self) -> bool { pub fn is_blank(&self) -> bool {
matches!(self, Tile::Blank) matches!(self, Tile::Blank)
} }

View file

@ -1,15 +1,17 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)] use crate::level::Level;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Solution { pub struct Solution {
solution_id: String, // solution_id: String,
level_id: String, // redundant? level_id: String, // redundant?
pub name: String, pub name: String,
pub board: String, pub board: String,
pub score: Option<Score>, pub score: Option<Score>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Score { pub struct Score {
pub cycles: u32, pub cycles: u32,
pub tiles: u32, pub tiles: u32,
@ -17,16 +19,23 @@ pub struct Score {
} }
impl Solution { impl Solution {
pub fn new(level_id: String, number: usize) -> Self { pub fn new(level: &Level, number: usize) -> Self {
Self { Self {
solution_id: format!("solution_{number}"), // solution_id: format!("solution_{number}"),
level_id, level_id: level.id().to_owned(),
name: format!("Unnamed {number}"), name: format!("Unnamed {number}"),
board: " \n".repeat(20), // todo remove when auto resizing is implemented board: level
.init_board()
.unwrap_or_else(|| " \n".repeat(20)), // todo remove when auto resizing is implemented
// score: Some(Score { cycles: 5, tiles: 88, area: 987 }), // score: Some(Score { cycles: 5, tiles: 88, area: 987 }),
score: None, score: None,
} }
} }
pub fn level_id(&self) -> &str {
&self.level_id
}
pub fn score_text(&self) -> String { pub fn score_text(&self) -> String {
if let Some(score) = &self.score { if let Some(score) = &self.score {
format!( format!(

View file

@ -122,7 +122,7 @@ pub fn text_input(
if d.is_key_pressed(KeyboardKey::KEY_ESCAPE) { if d.is_key_pressed(KeyboardKey::KEY_ESCAPE) {
*is_selected = false; *is_selected = false;
} }
if d.is_key_pressed(KeyboardKey::KEY_BACKSPACE) { if d.is_key_pressed(KeyboardKey::KEY_BACKSPACE) && !text.is_empty() {
changed = true; changed = true;
text.pop(); text.pop();
} }