Compare commits
26 commits
Author | SHA1 | Date | |
---|---|---|---|
1cc8e999b7 | |||
7d9d0a01f7 | |||
1eb7a34537 | |||
c8fc484e9c | |||
440cd7a759 | |||
2c7e844d00 | |||
80ab3b676e | |||
f0b878e93d | |||
509b577aaa | |||
62fcb538a6 | |||
df58b62712 | |||
0223250c88 | |||
5f5a831569 | |||
7222993156 | |||
31783cc10f | |||
44494f4d01 | |||
454c836b38 | |||
563c819900 | |||
522a027f7a | |||
69fe8546f5 | |||
8bc770b564 | |||
a755678567 | |||
660746f16b | |||
48b5b3f3c7 | |||
2a0483c156 | |||
187020fc27 |
18 changed files with 811 additions and 350 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,3 +2,5 @@
|
|||
/user*
|
||||
*.zip
|
||||
version.txt
|
||||
perf.data*
|
||||
flamegraph.svg
|
||||
|
|
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -1,11 +1,43 @@
|
|||
# Marble Machinations Change Log
|
||||
Game store page: https://crispypin.itch.io/marble-machinations
|
||||
|
||||
## v0.3.3 - 2025-04-21
|
||||
### added
|
||||
- option to display power direction while overlay is enabled
|
||||
- show level name in end popup
|
||||
|
||||
### changed
|
||||
- hide tick timing numbers by default
|
||||
- when multiple I/O silos (or multiple directions of one) are activated in the same tick, they will all output the same value instead of pulling input in an arbitrary order
|
||||
|
||||
### fixed
|
||||
- input bytes are consumed even if the marble can't be created because another one was taking its place
|
||||
- keybindings activated even when typing in a text field, making especially renaming blueprints difficult
|
||||
- grid rendering broken on right edge of the screen at some zoom levels and window sizes
|
||||
- crash when saving config if no user dir exists
|
||||
- bindings did not properly take into account order of pressing, so Shift+A and A+Shift were treated as the same thing
|
||||
- after removing a binding that was a superset of another, the remaining one did not stop being blocked by the removed ones additional modifiers until another binding was added or edited
|
||||
|
||||
## v0.3.2 - 2025-04-14
|
||||
### added
|
||||
- "weird machines" chapter with levels for [deadfish](https://esolangs.org/wiki/Deadfish) and [brainfuck](https://esolangs.org/wiki/Brainfuck)
|
||||
- "missing levels" section giving access to solutions to levels that are no longer available
|
||||
- click to collapse chapters in level list
|
||||
- input bindings for eraser (X), selection (B), blueprint list (Ctrl B), no tool (no default binding)
|
||||
### fixed
|
||||
- invalid action ids in the config file key bindings caused everything to revert to default.
|
||||
- when start and stop are bound to the same thing (as by default), only start works
|
||||
- when two input bindings had the same trigger but one has a strict subset of the others modifiers, both would activate when the one with more modifiers was pressed. For example (Ctrl+S -> Save) would also trigger (S -> Wire Tool). Now, Shift+S will still trigger Wire Tool, unless Shift+S (or eg. Shift+Ctrl+S) is bound to something else.
|
||||
|
||||
## v0.3.1 - 2025-04-05
|
||||
### fixed
|
||||
- broken area calculation causing crash when completing a level with a machine wider than it is tall
|
||||
|
||||
## v0.3.0 - 2025-04-04
|
||||
### added
|
||||
- score number: bounding area
|
||||
- configurable key bindings for many editor actions
|
||||
- QWERTY+ASDFGH keybindings for the tile tools
|
||||
- QWERTY+ASDFGH key bindings for the tile tools by default
|
||||
- OS clipboard copy/paste, with fallback to old behavior when copying
|
||||
- cut selection
|
||||
- in-grid text comments (not yet editable in-game)
|
||||
|
|
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -1,6 +1,6 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
|
@ -213,7 +213,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
|||
|
||||
[[package]]
|
||||
name = "marble-machinations"
|
||||
version = "0.3.0"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"raylib",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "marble-machinations"
|
||||
version = "0.3.0"
|
||||
version = "0.3.3"
|
||||
edition = "2021"
|
||||
default-run = "marble-machinations"
|
||||
|
||||
|
@ -10,6 +10,10 @@ raylib = "5.5"
|
|||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_json = "1.0.128"
|
||||
|
||||
[features]
|
||||
# affects simulation sub-steps, used for profiling
|
||||
inline_less = []
|
||||
|
||||
[[bin]]
|
||||
name = "bench"
|
||||
path = "src/benchmark.rs"
|
||||
|
|
6
Makefile
6
Makefile
|
@ -22,3 +22,9 @@ windows:
|
|||
rm -rf ${RELEASE_DIRNAME}_win
|
||||
|
||||
all: windows linux
|
||||
|
||||
flamegraph:
|
||||
cargo flamegraph --release --bin bench --features "inline_less"
|
||||
|
||||
bench:
|
||||
cargo run --release --bin bench
|
||||
|
|
42
README.md
42
README.md
|
@ -7,34 +7,52 @@ logic mostly like https://git.crispypin.cc/CrispyPin/marble
|
|||
## todo
|
||||
### meta
|
||||
- engine tests
|
||||
- blag post about marble movement logic
|
||||
### game
|
||||
- blag post about marble movement logic?
|
||||
- standardise terminology (cycle/step/tick)
|
||||
### bugs
|
||||
|
||||
### features
|
||||
#### 0.3.x
|
||||
- more levels
|
||||
- packet routing?
|
||||
- game of life sim (width;height;steps;grid -> grid)
|
||||
- shrink button
|
||||
#### 0.4.x
|
||||
- UI layout engine
|
||||
- global scale multiplier, affected by window size
|
||||
- background colour setting (requires color picker => after UI rework)
|
||||
- light theme
|
||||
#### unspecified
|
||||
- comments
|
||||
- editing
|
||||
- add to all intro levels
|
||||
- UI layout engine
|
||||
- global scale setting
|
||||
- highlight regions with background colours
|
||||
- accessibility
|
||||
- background colour setting
|
||||
- hotkeys for everything (no mouse needed to play)
|
||||
- more levels
|
||||
- scroll output bytes
|
||||
- button + binding to flip selection that is being pasted
|
||||
- hotkeys for everything (no mouse needed to play)
|
||||
- menu navigation (requires UI rework)
|
||||
- speed up/down
|
||||
- keybinds for specific tool variants
|
||||
- grid cursor movement and placement
|
||||
- grid zoom and pan
|
||||
- config settings page categories (mostly for keybindings)
|
||||
- UI: scroll output bytes
|
||||
- timestamps in solutions and blueprints
|
||||
- lock tile types for early levels to make it less overwhelming
|
||||
- display tool variant more clearly (it's not obvious there are more states)
|
||||
- better text rendering
|
||||
- font selection (probably a lot of work)
|
||||
### online stuff
|
||||
- store scores in server
|
||||
- validate solutions in server (with limits)
|
||||
- validate solutions in server (with compute limits)
|
||||
- show histograms
|
||||
- author name in solutions and blueprints
|
||||
### undecided
|
||||
- option to skip (fast-forward through with settable multiplier) first N stages or cycles, for when you are debugging something that happens in later stages
|
||||
- hide some tile tools in early levels to make it less overwhelming
|
||||
- footprint score (tiles that were non-empty at any point in the run)
|
||||
- option to use 8-bit marbles?
|
||||
- blueprint rotation?
|
||||
- 32 bit input/output?
|
||||
- settable marble start direction?
|
||||
- allows blueprint rotation
|
||||
|
||||
## playtesting observations
|
||||
- 'loops' introduces too many things (powering, redirection, generating zeroes)
|
||||
|
|
44
levels/chapter_05.json
Normal file
44
levels/chapter_05.json
Normal file
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"title": "5. Weird machines",
|
||||
"levels": [
|
||||
{
|
||||
"id": "deadfish",
|
||||
"name": "Deadfish",
|
||||
"description": "Deadfish is a very small joke programming language. It is often used as a test program for other esoteric programming languages. Marble machinations is now one of them.\n\n There are four commands:\ni: increment the accumulator\nd: decrement the accumulator\ns: square the accumulator\no: output the accumulator\n\n if the value becomes -1 or 256, it should be set to zero.\n\nThe test cases are taken from esolangs.org/wiki/Deadfish",
|
||||
"stages": [{
|
||||
"input": "iiso",
|
||||
"output": "4"
|
||||
},{
|
||||
"input": "iissso",
|
||||
"output": "0"
|
||||
},{
|
||||
"input": "diissisdo",
|
||||
"output": "288"
|
||||
},{
|
||||
"input": "iissisdddddddddddddddddddddddddddddddddo",
|
||||
"output": "0"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"id": "brainfuck",
|
||||
"name": "Brainfuck",
|
||||
"description": "The language brainfuck operates on a 'tape' of bytes, with a pointer that can be moved left and right. Level input is formatted as '<program>#<input>'.\n(separator # is 35)\n\n+ (43): increment\n- (45): decrement\n< (60): move left\n> (62): move right\n, (44): input\n. (46): output\n[ (91): skip to matching ] if current value is zero\n] (93): jump to matching [ if current value is not zero\n\nmaximum memory needed: 32 bytes\nlongest program: 106 commands",
|
||||
"stages":[{
|
||||
"input": ">><[]++++++++-.#",
|
||||
"output": [7]
|
||||
},{
|
||||
"input": ",>,>,>,.<.<.<.#woem",
|
||||
"output": "meow"
|
||||
},{
|
||||
"input": "++++[++++.]#",
|
||||
"output": [8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 148, 152, 156, 160, 164, 168, 172, 176, 180, 184, 188, 192, 196, 200, 204, 208, 212, 216, 220, 224, 228, 232, 236, 240, 244, 248, 252, 0]
|
||||
},{
|
||||
"input": "-[>,]<+[-.<+]-#reverse cat program",
|
||||
"output": "margorp tac esrever"
|
||||
},{
|
||||
"input": "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.#",
|
||||
"output": "Hello World!\n"
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,11 +1,21 @@
|
|||
use raylib::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{input::Input, theme::FG_CHAPTER_TITLE, ui::text_button, util::Scroll, Globals};
|
||||
use crate::{
|
||||
input::Input,
|
||||
theme::FG_CHAPTER_TITLE,
|
||||
ui::{text_button, toggle_button},
|
||||
util::Scroll,
|
||||
Globals,
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub input: Input,
|
||||
#[serde(default)]
|
||||
pub show_debug_timing: bool,
|
||||
#[serde(default)]
|
||||
pub show_power_direction: bool,
|
||||
#[serde(skip)]
|
||||
scroll_offset: u32,
|
||||
}
|
||||
|
@ -25,20 +35,31 @@ impl Config {
|
|||
Some(Scroll::Up) => self.scroll_offset = self.scroll_offset.saturating_sub(64),
|
||||
None => (),
|
||||
}
|
||||
let y = -(self.scroll_offset as i32);
|
||||
let mut y = -(self.scroll_offset as i32) + 15;
|
||||
d.draw_text("Settings", 16, y, 30, FG_CHAPTER_TITLE);
|
||||
y += 40;
|
||||
|
||||
d.draw_text("Settings", 16, y + 16, 30, FG_CHAPTER_TITLE);
|
||||
|
||||
if text_button(d, &globals.mouse, 10, y + 60, 80, "apply") {
|
||||
if text_button(d, &globals.mouse, 10, y, 80, "apply") {
|
||||
return MenuReturn::StaySave;
|
||||
}
|
||||
if text_button(d, &globals.mouse, 100, y + 60, 80, "done") {
|
||||
if text_button(d, &globals.mouse, 100, y, 80, "done") {
|
||||
return MenuReturn::ReturnSave;
|
||||
}
|
||||
if text_button(d, &globals.mouse, 190, y + 60, 80, "cancel") {
|
||||
if text_button(d, &globals.mouse, 190, y, 80, "cancel") {
|
||||
return MenuReturn::ReturnCancel;
|
||||
}
|
||||
y += 40;
|
||||
|
||||
let mut toggle = |value, text| {
|
||||
toggle_button((d, &globals.mouse), 10, y, 30, 30, value);
|
||||
d.draw_text(text, 50, y + 5, 20, Color::WHITE);
|
||||
y += 40;
|
||||
};
|
||||
|
||||
toggle(&mut self.show_power_direction, "show power directions");
|
||||
toggle(&mut self.show_debug_timing, "show debug timing");
|
||||
|
||||
// self.input.update(d);
|
||||
self.input.draw_edit(d, globals, y);
|
||||
MenuReturn::Stay
|
||||
}
|
||||
|
|
278
src/editor.rs
278
src/editor.rs
|
@ -24,7 +24,7 @@ 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 = 225;
|
||||
const END_POPUP_HEIGHT: i32 = 255;
|
||||
|
||||
const MAX_ZOOM: f32 = 8.;
|
||||
const MIN_ZOOM: f32 = 0.25;
|
||||
|
@ -74,8 +74,8 @@ pub struct Editor {
|
|||
undo_history: Vec<Action>,
|
||||
undo_index: usize,
|
||||
// debug/profiling
|
||||
step_time: u128,
|
||||
max_step_time: u128,
|
||||
step_time: usize,
|
||||
max_step_time: usize,
|
||||
start_time: Instant,
|
||||
}
|
||||
|
||||
|
@ -85,10 +85,10 @@ enum Action {
|
|||
SetArea(ResizeDeltas, Pos, Board, Board),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
enum Popup {
|
||||
None,
|
||||
Success,
|
||||
Success(Score),
|
||||
Failure,
|
||||
LevelInfo,
|
||||
PauseMenu,
|
||||
|
@ -316,7 +316,7 @@ impl Editor {
|
|||
self.machine.step();
|
||||
|
||||
if let Some(i) = self.stage {
|
||||
if matches!(self.popup, Popup::Failure | Popup::Success) {
|
||||
if matches!(self.popup, Popup::Failure | Popup::Success(_)) {
|
||||
self.popup = Popup::None;
|
||||
self.dismissed_end = true;
|
||||
}
|
||||
|
@ -328,15 +328,16 @@ impl Editor {
|
|||
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 {
|
||||
let score = Score {
|
||||
cycles: self.total_steps + self.machine.step_count(),
|
||||
tiles: self.source_board.grid.count_tiles(),
|
||||
bounds_area: self.source_board.grid.used_bounds_area(),
|
||||
});
|
||||
};
|
||||
self.score = Some(score.clone());
|
||||
self.popup = Popup::Success(score);
|
||||
}
|
||||
} else if !stage.output().as_bytes().starts_with(self.machine.output()) {
|
||||
self.popup = Popup::Failure;
|
||||
|
@ -439,7 +440,7 @@ impl Editor {
|
|||
|
||||
if globals.is_pressed(ActionId::ToggleMenu) {
|
||||
self.popup = match self.popup {
|
||||
Popup::Success | Popup::Failure => {
|
||||
Popup::Success(_) | Popup::Failure => {
|
||||
self.dismissed_end = true;
|
||||
Popup::None
|
||||
}
|
||||
|
@ -472,28 +473,31 @@ impl Editor {
|
|||
.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);
|
||||
self.step_time = avg_step_time as usize;
|
||||
self.max_step_time = self.step_time.max(self.max_step_time);
|
||||
}
|
||||
if globals.is_pressed(ActionId::StepSim) {
|
||||
self.step_pressed()
|
||||
}
|
||||
if globals.is_pressed(ActionId::StartSim) {
|
||||
match self.sim_state {
|
||||
SimState::Editing => {
|
||||
match self.sim_state {
|
||||
SimState::Editing => {
|
||||
if globals.is_pressed(ActionId::StartSim) {
|
||||
self.init_sim();
|
||||
self.sim_state = SimState::Running;
|
||||
}
|
||||
SimState::Stepping => self.sim_state = SimState::Running,
|
||||
SimState::Running => (),
|
||||
}
|
||||
} else if globals.is_pressed(ActionId::StopSim) {
|
||||
match self.sim_state {
|
||||
SimState::Running | SimState::Stepping => {
|
||||
SimState::Stepping => {
|
||||
if globals.is_pressed(ActionId::StartSim) {
|
||||
self.sim_state = SimState::Running
|
||||
} else if globals.is_pressed(ActionId::StopSim) {
|
||||
self.sim_state = SimState::Editing;
|
||||
self.popup = Popup::None;
|
||||
}
|
||||
SimState::Editing => (),
|
||||
}
|
||||
SimState::Running => {
|
||||
if globals.is_pressed(ActionId::StopSim) {
|
||||
self.sim_state = SimState::Editing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -571,27 +575,48 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
fn draw_board(&self, d: &mut RaylibDrawHandle, textures: &Textures) {
|
||||
fn draw_board(&self, d: &mut RaylibDrawHandle, globals: &Globals) {
|
||||
let draw_power = globals.config.show_power_direction && self.draw_overlay;
|
||||
if self.sim_state == SimState::Editing {
|
||||
self.source_board
|
||||
.grid
|
||||
.draw(d, textures, self.view_offset, self.zoom);
|
||||
self.source_board.grid.draw(
|
||||
d,
|
||||
&globals.textures,
|
||||
self.view_offset,
|
||||
self.zoom,
|
||||
draw_power,
|
||||
);
|
||||
} else {
|
||||
if self.machine.debug_subticks.is_empty() {
|
||||
self.machine
|
||||
.grid()
|
||||
.draw(d, textures, self.view_offset, self.zoom);
|
||||
self.machine.grid().draw(
|
||||
d,
|
||||
&globals.textures,
|
||||
self.view_offset,
|
||||
self.zoom,
|
||||
draw_power,
|
||||
);
|
||||
} else {
|
||||
let subframe = &self.machine.debug_subticks[self.machine.subtick_index];
|
||||
subframe.grid.draw(d, textures, self.view_offset, self.zoom);
|
||||
subframe.grid.draw(
|
||||
d,
|
||||
&globals.textures,
|
||||
self.view_offset,
|
||||
self.zoom,
|
||||
draw_power,
|
||||
);
|
||||
if let Some(pos) = subframe.pos {
|
||||
let p = self.pos_to_screen(pos.to_vec());
|
||||
d.draw_texture_ex(textures.get("selection"), p, 0., self.zoom, Color::ORANGE);
|
||||
d.draw_texture_ex(
|
||||
globals.get_tex("selection"),
|
||||
p,
|
||||
0.,
|
||||
self.zoom,
|
||||
Color::ORANGE,
|
||||
);
|
||||
}
|
||||
}
|
||||
if self.draw_overlay {
|
||||
self.machine
|
||||
.draw_marble_values(d, textures, self.view_offset, self.zoom);
|
||||
.draw_marble_values(d, &globals.textures, self.view_offset, self.zoom);
|
||||
}
|
||||
}
|
||||
if self.draw_overlay {
|
||||
|
@ -607,23 +632,25 @@ impl Editor {
|
|||
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 hlines = d.get_screen_height() / tile_size as i32 + 3;
|
||||
let vlines = d.get_screen_width() / tile_size as i32 + 3;
|
||||
for y in 0..hlines {
|
||||
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) {
|
||||
for x in 0..vlines {
|
||||
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, &globals.textures);
|
||||
self.board_overlay(d, &globals.textures);
|
||||
self.draw_board(d, globals);
|
||||
self.board_overlay(d, globals);
|
||||
self.draw_bottom_bar(d, globals);
|
||||
self.draw_top_bar(d, globals);
|
||||
|
||||
if self.active_tool == Tool::Blueprint {
|
||||
self.draw_blueprint_sidebar(d, &globals.textures);
|
||||
self.draw_blueprint_sidebar(d, globals);
|
||||
}
|
||||
|
||||
self.mouse.update(d);
|
||||
|
@ -640,7 +667,7 @@ impl Editor {
|
|||
}
|
||||
|
||||
match self.popup {
|
||||
Popup::Success | Popup::Failure => {
|
||||
Popup::Success(_) | Popup::Failure => {
|
||||
self.draw_end_popup(d, &globals.textures);
|
||||
}
|
||||
Popup::LevelInfo => {
|
||||
|
@ -673,7 +700,7 @@ impl Editor {
|
|||
self.tooltip.draw(d);
|
||||
}
|
||||
|
||||
fn draw_blueprint_sidebar(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) {
|
||||
fn draw_blueprint_sidebar(&mut self, d: &mut RaylibDrawHandle, globals: &mut Globals) {
|
||||
let sidebar_height = d.get_screen_height() - FOOTER_HEIGHT - HEADER_HEIGHT - 40;
|
||||
d.draw_rectangle(
|
||||
0,
|
||||
|
@ -697,7 +724,7 @@ impl Editor {
|
|||
if tex32_button(
|
||||
(d, &self.mouse),
|
||||
(5, y),
|
||||
textures.get("rubbish"),
|
||||
globals.get_tex("rubbish"),
|
||||
(&mut self.tooltip, "Delete"),
|
||||
) {
|
||||
b.remove_file();
|
||||
|
@ -708,7 +735,7 @@ impl Editor {
|
|||
let mut text_selected = is_selected && self.blueprint_name_selected;
|
||||
text_input(
|
||||
d,
|
||||
&self.mouse,
|
||||
globals,
|
||||
Rectangle::new(42., y as f32, 200., 32.),
|
||||
&mut b.name,
|
||||
&mut text_selected,
|
||||
|
@ -727,7 +754,7 @@ impl Editor {
|
|||
);
|
||||
|
||||
d.draw_texture_ex(
|
||||
textures.get("blueprint"),
|
||||
globals.get_tex("blueprint"),
|
||||
Vector2::new((42 + 205) as f32, y as f32),
|
||||
0.,
|
||||
2.,
|
||||
|
@ -740,38 +767,35 @@ impl Editor {
|
|||
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;
|
||||
let mut y = bounds.y as i32 + 10;
|
||||
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 + 40, 20, Color::WHITE);
|
||||
draw_usize(d, textures, score.cycles, (x + 110, y + 40), 9, 2);
|
||||
d.draw_text("tiles", x + 15, y + 80, 20, Color::WHITE);
|
||||
draw_usize(d, textures, score.tiles, (x + 110, y + 80), 9, 2);
|
||||
d.draw_text("bounds", x + 15, y + 120, 20, Color::WHITE);
|
||||
draw_usize(d, textures, score.bounds_area, (x + 110, y + 120), 9, 2);
|
||||
}
|
||||
let y = y + 60;
|
||||
if simple_button((d, &self.mouse), x + 10, y + 110, 140, 45) {
|
||||
if let Popup::Success(score) = &self.popup {
|
||||
d.draw_text("Level Complete!", x + 45, y, 30, Color::LIME);
|
||||
y += 30;
|
||||
d.draw_text(self.level.name(), x + 10, y, 20, Color::GRAY);
|
||||
y += 30;
|
||||
d.draw_text("cycles", x + 15, y, 20, Color::WHITE);
|
||||
draw_usize(d, textures, score.cycles, (x + 110, y), 9, 2);
|
||||
y += 40;
|
||||
d.draw_text("tiles", x + 15, y, 20, Color::WHITE);
|
||||
draw_usize(d, textures, score.tiles, (x + 110, y), 9, 2);
|
||||
y += 40;
|
||||
d.draw_text("bounds", x + 15, y, 20, Color::WHITE);
|
||||
draw_usize(d, textures, score.bounds_area, (x + 110, y), 9, 2);
|
||||
y += 40;
|
||||
if simple_button((d, &self.mouse), x + 10, y, 140, 50) {
|
||||
self.popup = Popup::None;
|
||||
self.dismissed_end = true;
|
||||
}
|
||||
d.draw_text("continue\nediting", x + 15, y + 115, 20, Color::WHITE);
|
||||
d.draw_text("continue\nediting", x + 15, y + 5, 20, Color::WHITE);
|
||||
|
||||
if simple_button(
|
||||
(d, &self.mouse),
|
||||
x + END_POPUP_WIDTH / 2 + 5,
|
||||
y + 110,
|
||||
140,
|
||||
45,
|
||||
) {
|
||||
if simple_button((d, &self.mouse), x + END_POPUP_WIDTH / 2 + 5, y, 140, 50) {
|
||||
self.exit_state = ExitState::ExitAndSave;
|
||||
}
|
||||
d.draw_text(
|
||||
"return to\nlevel list",
|
||||
x + END_POPUP_WIDTH / 2 + 10,
|
||||
y + 115,
|
||||
y + 5,
|
||||
20,
|
||||
Color::WHITE,
|
||||
);
|
||||
|
@ -786,8 +810,7 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
fn draw_top_bar(&mut self, d: &mut RaylibDrawHandle, globals: &Globals) {
|
||||
let textures = &globals.textures;
|
||||
fn draw_top_bar(&mut self, d: &mut RaylibDrawHandle, globals: &mut Globals) {
|
||||
// background
|
||||
d.draw_rectangle(
|
||||
0,
|
||||
|
@ -800,7 +823,7 @@ impl Editor {
|
|||
if tex32_button(
|
||||
(d, &self.mouse),
|
||||
(4, 4),
|
||||
textures.get("exit"),
|
||||
globals.get_tex("exit"),
|
||||
(&mut self.tooltip, "exit"),
|
||||
) {
|
||||
if self.exit_menu {
|
||||
|
@ -813,7 +836,7 @@ impl Editor {
|
|||
if tex32_button(
|
||||
(d, &self.mouse),
|
||||
(40, 4),
|
||||
textures.get("cancel"),
|
||||
globals.get_tex("cancel"),
|
||||
(&mut self.tooltip, "cancel"),
|
||||
) {
|
||||
self.exit_menu = false;
|
||||
|
@ -821,7 +844,7 @@ impl Editor {
|
|||
} else if tex32_button(
|
||||
(d, &self.mouse),
|
||||
(40, 4),
|
||||
textures.get("save"),
|
||||
globals.get_tex("save"),
|
||||
(&mut self.tooltip, "save"),
|
||||
) || globals.is_pressed(ActionId::Save)
|
||||
{
|
||||
|
@ -841,7 +864,7 @@ impl Editor {
|
|||
if tex32_button(
|
||||
(d, &self.mouse),
|
||||
(150, 4),
|
||||
textures.get(undo_icon),
|
||||
globals.get_tex(undo_icon),
|
||||
(&mut self.tooltip, "Undo"),
|
||||
) {
|
||||
self.undo()
|
||||
|
@ -855,7 +878,7 @@ impl Editor {
|
|||
if tex32_button(
|
||||
(d, &self.mouse),
|
||||
(186, 4),
|
||||
textures.get(redo_icon),
|
||||
globals.get_tex(redo_icon),
|
||||
(&mut self.tooltip, "Redo"),
|
||||
) {
|
||||
self.redo()
|
||||
|
@ -870,7 +893,7 @@ impl Editor {
|
|||
if tex32_button(
|
||||
(d, &self.mouse),
|
||||
(223, 4),
|
||||
textures.get(overlay_btn_icon),
|
||||
globals.get_tex(overlay_btn_icon),
|
||||
(&mut self.tooltip, "Toggle overlay"),
|
||||
) {
|
||||
self.draw_overlay = !self.draw_overlay;
|
||||
|
@ -880,7 +903,7 @@ impl Editor {
|
|||
if tex32_button(
|
||||
(d, &self.mouse),
|
||||
(260, 4),
|
||||
textures.get("pause"),
|
||||
globals.get_tex("pause"),
|
||||
(&mut self.tooltip, "Pause"),
|
||||
) {
|
||||
self.sim_state = SimState::Stepping;
|
||||
|
@ -888,7 +911,7 @@ impl Editor {
|
|||
} else if tex32_button(
|
||||
(d, &self.mouse),
|
||||
(260, 4),
|
||||
textures.get("play"),
|
||||
globals.get_tex("play"),
|
||||
(&mut self.tooltip, "Start"),
|
||||
) {
|
||||
if self.sim_state == SimState::Editing {
|
||||
|
@ -901,7 +924,7 @@ impl Editor {
|
|||
&& tex32_button(
|
||||
(d, &self.mouse),
|
||||
(296, 4),
|
||||
textures.get("stop"),
|
||||
globals.get_tex("stop"),
|
||||
(&mut self.tooltip, "Stop"),
|
||||
) {
|
||||
self.sim_state = SimState::Editing;
|
||||
|
@ -911,14 +934,21 @@ impl Editor {
|
|||
if tex32_button(
|
||||
(d, &self.mouse),
|
||||
(332, 4),
|
||||
textures.get("step"),
|
||||
globals.get_tex("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);
|
||||
draw_usize(
|
||||
d,
|
||||
&globals.textures,
|
||||
1 << self.sim_speed,
|
||||
(368, 4),
|
||||
SPEED_DIGITS,
|
||||
1,
|
||||
);
|
||||
slider(
|
||||
(d, &self.mouse),
|
||||
rect(368, 24, 48, 12),
|
||||
|
@ -928,20 +958,36 @@ impl Editor {
|
|||
);
|
||||
|
||||
self.tooltip.add(420, 4, 180, 32, "Steps");
|
||||
draw_usize(d, textures, self.machine.step_count(), (420, 4), 9, 2);
|
||||
draw_usize(
|
||||
d,
|
||||
&globals.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, &globals.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);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
draw_usize(d, textures, self.machine.subtick_index, (260, 80), 9, 1);
|
||||
let subtick_count = self.machine.debug_subticks.len();
|
||||
draw_usize(d, textures, subtick_count, (260, 100), 9, 1);
|
||||
if globals.config.show_debug_timing {
|
||||
draw_usize(d, &globals.textures, self.step_time, (260, 42), 9, 1);
|
||||
draw_usize(d, &globals.textures, self.max_step_time, (260, 60), 9, 1);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
draw_usize(
|
||||
d,
|
||||
&globals.textures,
|
||||
self.machine.subtick_index,
|
||||
(260, 80),
|
||||
9,
|
||||
1,
|
||||
);
|
||||
let subtick_count = self.machine.debug_subticks.len();
|
||||
draw_usize(d, &globals.textures, subtick_count, (260, 100), 9, 1);
|
||||
}
|
||||
}
|
||||
|
||||
d.draw_text("input:", 603, 8, 10, Color::WHITE);
|
||||
|
@ -964,7 +1010,7 @@ impl Editor {
|
|||
}
|
||||
if text_input(
|
||||
d,
|
||||
&self.mouse,
|
||||
globals,
|
||||
Rectangle::new(input_x as f32, 5., (width - input_x - 5) as f32, 30.),
|
||||
&mut input_text,
|
||||
&mut self.input_text_selected,
|
||||
|
@ -1018,7 +1064,7 @@ impl Editor {
|
|||
hide_tile_tools = true;
|
||||
text_input(
|
||||
d,
|
||||
&self.mouse,
|
||||
globals,
|
||||
Rectangle::new(100., footer_top + 10., 240., 30.),
|
||||
&mut self.new_blueprint_name,
|
||||
&mut self.blueprint_name_selected,
|
||||
|
@ -1060,7 +1106,7 @@ impl Editor {
|
|||
|
||||
self.tooltip.add(276, y, 40, 40, "Delete");
|
||||
if simple_button((d, &self.mouse), 276, y, 40, 40)
|
||||
|| globals.is_pressed(ActionId::Erase)
|
||||
|| globals.is_pressed(ActionId::EraseSelection)
|
||||
{
|
||||
erase_selection = true;
|
||||
}
|
||||
|
@ -1091,7 +1137,7 @@ impl Editor {
|
|||
texture: &str,
|
||||
tooltip: &'static str,
|
||||
tool_option: Tool,
|
||||
action: Option<ActionId>| {
|
||||
action: ActionId| {
|
||||
let border = 4.;
|
||||
let gap = 2.;
|
||||
let button_size = 32. + border * 2.;
|
||||
|
@ -1111,20 +1157,26 @@ impl Editor {
|
|||
tool_option,
|
||||
&mut self.active_tool,
|
||||
border,
|
||||
action.map(|a| globals.is_pressed(a)).unwrap_or(false),
|
||||
globals.is_pressed(action),
|
||||
)
|
||||
};
|
||||
tool_button((0, -2), "eraser", "Eraser", Tool::Erase, None);
|
||||
tool_button((0, -2), "eraser", "Eraser", Tool::Erase, ActionId::Eraser);
|
||||
tool_button(
|
||||
(1, -2),
|
||||
"selection",
|
||||
"Select",
|
||||
Tool::SelectArea(Selection::default()),
|
||||
None,
|
||||
ActionId::Selection,
|
||||
);
|
||||
|
||||
tool_button((0, -1), "blueprint", "Blueprints", Tool::Blueprint, None);
|
||||
tool_button((1, -1), "transparent", "None", Tool::None, None);
|
||||
tool_button(
|
||||
(0, -1),
|
||||
"blueprint",
|
||||
"Blueprints",
|
||||
Tool::Blueprint,
|
||||
ActionId::Blueprints,
|
||||
);
|
||||
tool_button((1, -1), "transparent", "None", Tool::None, ActionId::NoTool);
|
||||
|
||||
if !hide_tile_tools {
|
||||
tool_button(
|
||||
|
@ -1132,42 +1184,42 @@ impl Editor {
|
|||
"block",
|
||||
"Block",
|
||||
Tool::SetTile(Tile::from_char('#')),
|
||||
Some(ActionId::TileBlock),
|
||||
ActionId::TileBlock,
|
||||
);
|
||||
tool_button(
|
||||
(0, 1),
|
||||
"silo_off",
|
||||
"Silo",
|
||||
Tool::SetTile(Tile::from_char('B')),
|
||||
Some(ActionId::TileSilo),
|
||||
ActionId::TileSilo,
|
||||
);
|
||||
tool_button(
|
||||
(0, 2),
|
||||
"button_off",
|
||||
"Button",
|
||||
Tool::SetTile(Tile::from_char('*')),
|
||||
Some(ActionId::TileButton),
|
||||
ActionId::TileButton,
|
||||
);
|
||||
tool_button(
|
||||
(0, 3),
|
||||
"io_tile_off",
|
||||
"Input/Output silo",
|
||||
Tool::SetTile(Tile::from_char('I')),
|
||||
Some(ActionId::TileIOSilo),
|
||||
ActionId::TileIOSilo,
|
||||
);
|
||||
tool_button(
|
||||
(0, 4),
|
||||
"flipper_off",
|
||||
"Flipper",
|
||||
Tool::SetTile(Tile::from_char('F')),
|
||||
Some(ActionId::TileFlipper),
|
||||
ActionId::TileFlipper,
|
||||
);
|
||||
tool_button(
|
||||
(0, 5),
|
||||
"digit_tool",
|
||||
"Digit",
|
||||
Tool::Digits(None),
|
||||
Some(ActionId::TileDigit),
|
||||
ActionId::TileDigit,
|
||||
);
|
||||
|
||||
tool_button(
|
||||
|
@ -1175,14 +1227,14 @@ impl Editor {
|
|||
"marble",
|
||||
"Marble",
|
||||
Tool::SetTile(Tile::from_char('o')),
|
||||
Some(ActionId::TileMarble),
|
||||
ActionId::TileMarble,
|
||||
);
|
||||
match tool_button(
|
||||
(1, 1),
|
||||
self.tool_wire.texture_name_off(),
|
||||
self.tool_wire.human_name(),
|
||||
Tool::Wire,
|
||||
Some(ActionId::TileGroupWire),
|
||||
ActionId::TileGroupWire,
|
||||
) {
|
||||
Some(Scroll::Down) => self.tool_wire.next(),
|
||||
Some(Scroll::Up) => self.tool_wire.prev(),
|
||||
|
@ -1194,7 +1246,7 @@ impl Editor {
|
|||
self.tool_arrow.arrow_tile_texture_name(),
|
||||
self.tool_arrow.arrow_tile_human_name(),
|
||||
Tool::Arrow,
|
||||
Some(ActionId::TileGroupArrow),
|
||||
ActionId::TileGroupArrow,
|
||||
) {
|
||||
Some(Scroll::Down) => self.tool_arrow = self.tool_arrow.right(),
|
||||
Some(Scroll::Up) => self.tool_arrow = self.tool_arrow.left(),
|
||||
|
@ -1205,7 +1257,7 @@ impl Editor {
|
|||
self.tool_mirror.texture_name(),
|
||||
self.tool_mirror.human_name(),
|
||||
Tool::Mirror,
|
||||
Some(ActionId::TileGroupMirror),
|
||||
ActionId::TileGroupMirror,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
|
@ -1216,7 +1268,7 @@ impl Editor {
|
|||
self.tool_math.texture_name_off(),
|
||||
self.tool_math.human_name(),
|
||||
Tool::Math,
|
||||
Some(ActionId::TileGroupMath),
|
||||
ActionId::TileGroupMath,
|
||||
) {
|
||||
Some(Scroll::Down) => self.tool_math.next(),
|
||||
Some(Scroll::Up) => self.tool_math.prev(),
|
||||
|
@ -1227,7 +1279,7 @@ impl Editor {
|
|||
self.tool_comparator.texture_name_off(),
|
||||
self.tool_comparator.human_name(),
|
||||
Tool::Comparator,
|
||||
Some(ActionId::TileGroupCompare),
|
||||
ActionId::TileGroupCompare,
|
||||
) {
|
||||
Some(Scroll::Down) => self.tool_comparator.next(),
|
||||
Some(Scroll::Up) => self.tool_comparator.prev(),
|
||||
|
@ -1308,7 +1360,7 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
fn board_overlay(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) {
|
||||
fn board_overlay(&mut self, d: &mut RaylibDrawHandle, globals: &Globals) {
|
||||
let footer_top = (d.get_screen_height() - FOOTER_HEIGHT) as f32;
|
||||
|
||||
let tile_size = TILE_TEXTURE_SIZE * self.zoom;
|
||||
|
@ -1327,7 +1379,9 @@ impl Editor {
|
|||
offset.x -= offset.x.rem(tile_size);
|
||||
offset.y -= offset.y.rem(tile_size);
|
||||
offset += view_offset;
|
||||
board.grid.draw(d, textures, offset, self.zoom);
|
||||
board
|
||||
.grid
|
||||
.draw(d, &globals.textures, offset, self.zoom, false);
|
||||
board.draw_comments(d, offset, self.zoom);
|
||||
if self.mouse.left_click() {
|
||||
let tile_pos = (self.mouse.pos() - self.view_offset) / tile_size;
|
||||
|
@ -1341,7 +1395,7 @@ impl Editor {
|
|||
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"),
|
||||
globals.get_tex("selection"),
|
||||
tile_screen_pos,
|
||||
0.,
|
||||
self.zoom,
|
||||
|
@ -1394,7 +1448,7 @@ impl Editor {
|
|||
};
|
||||
|
||||
d.draw_texture_ex(
|
||||
textures.get(tex),
|
||||
globals.get_tex(tex),
|
||||
tile_screen_pos,
|
||||
0.,
|
||||
self.zoom,
|
||||
|
@ -1464,7 +1518,9 @@ impl Editor {
|
|||
offset.x -= offset.x.rem(tile_size);
|
||||
offset.y -= offset.y.rem(tile_size);
|
||||
offset += view_offset;
|
||||
bp.board.grid.draw(d, textures, offset, self.zoom);
|
||||
bp.board
|
||||
.grid
|
||||
.draw(d, &globals.textures, offset, self.zoom, false);
|
||||
bp.board.draw_comments(d, offset, self.zoom);
|
||||
}
|
||||
if self.mouse.pos().x < SIDEBAR_WIDTH as f32 {
|
||||
|
|
186
src/input.rs
186
src/input.rs
|
@ -23,7 +23,7 @@ pub enum ActionId {
|
|||
Copy,
|
||||
Cut,
|
||||
Paste,
|
||||
Erase,
|
||||
EraseSelection,
|
||||
ToggleMenu,
|
||||
Save,
|
||||
StartSim,
|
||||
|
@ -32,6 +32,10 @@ pub enum ActionId {
|
|||
CycleGroup,
|
||||
CycleGroupRevMod,
|
||||
|
||||
Eraser,
|
||||
Selection,
|
||||
Blueprints,
|
||||
NoTool,
|
||||
TileBlock,
|
||||
TileSilo,
|
||||
TileButton,
|
||||
|
@ -54,6 +58,7 @@ impl Default for Input {
|
|||
let mut bindings = [(); ActionId::SIZE].map(|_| Vec::new());
|
||||
let mut bind_key = |action, mods, trigger| {
|
||||
bindings[action as usize].push(Binding {
|
||||
blocking_modifiers: Vec::new(),
|
||||
modifiers: mods,
|
||||
trigger,
|
||||
});
|
||||
|
@ -64,7 +69,7 @@ impl Default for Input {
|
|||
bind_key(ActionId::Copy, vec![LCtrl], C);
|
||||
bind_key(ActionId::Cut, vec![LCtrl], X);
|
||||
bind_key(ActionId::Paste, vec![LCtrl], V);
|
||||
bind_key(ActionId::Erase, vec![], Backspace);
|
||||
bind_key(ActionId::EraseSelection, vec![], Backspace);
|
||||
bind_key(ActionId::ToggleMenu, vec![], Escape);
|
||||
bind_key(ActionId::Save, vec![LCtrl], S);
|
||||
bind_key(ActionId::StartSim, vec![], Enter);
|
||||
|
@ -72,6 +77,9 @@ impl Default for Input {
|
|||
bind_key(ActionId::StepSim, vec![], Space);
|
||||
bind_key(ActionId::CycleGroup, vec![], Tab);
|
||||
bind_key(ActionId::CycleGroupRevMod, vec![], LShift);
|
||||
bind_key(ActionId::Eraser, vec![], X);
|
||||
bind_key(ActionId::Blueprints, vec![LCtrl], B);
|
||||
bind_key(ActionId::Selection, vec![], B);
|
||||
bind_key(ActionId::TileBlock, vec![], Q);
|
||||
bind_key(ActionId::TileSilo, vec![], W);
|
||||
bind_key(ActionId::TileButton, vec![], E);
|
||||
|
@ -86,9 +94,11 @@ impl Default for Input {
|
|||
bind_key(ActionId::TileGroupCompare, vec![], H);
|
||||
|
||||
Self {
|
||||
bindings,
|
||||
states: Default::default(),
|
||||
action_bindings: bindings,
|
||||
action_states: [BindingState::Off; ActionId::SIZE],
|
||||
key_states: [BindingState::Off; Button::SIZE],
|
||||
editing_binding: None,
|
||||
in_text_edit: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,14 +112,16 @@ enum BindingState {
|
|||
Released,
|
||||
}
|
||||
|
||||
type InputMap = BTreeMap<ActionId, Vec<Binding>>;
|
||||
type InputMap = BTreeMap<String, Vec<Binding>>;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(from = "InputMap", into = "InputMap")]
|
||||
pub struct Input {
|
||||
bindings: [Vec<Binding>; ActionId::SIZE],
|
||||
states: [BindingState; ActionId::SIZE],
|
||||
action_bindings: [Vec<Binding>; ActionId::SIZE],
|
||||
action_states: [BindingState; ActionId::SIZE],
|
||||
key_states: [BindingState; Button::SIZE],
|
||||
editing_binding: Option<(ActionId, usize, BindingEdit)>,
|
||||
pub in_text_edit: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -120,8 +132,7 @@ enum BindingEdit {
|
|||
}
|
||||
|
||||
impl Input {
|
||||
pub fn draw_edit(&mut self, d: &mut RaylibDrawHandle, globals: &mut Globals, y: i32) {
|
||||
let mut y = y + 96;
|
||||
pub fn draw_edit(&mut self, d: &mut RaylibDrawHandle, globals: &mut Globals, mut y: i32) {
|
||||
if self.editing_binding.is_some() {
|
||||
globals.mouse.clear();
|
||||
}
|
||||
|
@ -132,36 +143,48 @@ impl Input {
|
|||
for action_index in 0..ActionId::SIZE {
|
||||
let action = ActionId::from_usize(action_index).unwrap();
|
||||
|
||||
// if self.action_states[action_index] == BindingState::Pressed {
|
||||
// d.draw_rectangle(200, y, 20, 20, Color::LIMEGREEN);
|
||||
// }
|
||||
d.draw_text(&format!("{action:?}"), 16, y, 20, Color::ORANGE);
|
||||
for (binding_index, binding) in self.bindings[action_index].iter().enumerate() {
|
||||
for (binding_index, binding) in self.action_bindings[action_index].iter().enumerate() {
|
||||
if text_button(d, &globals.mouse, buttons_x, y, 80, "remove") {
|
||||
self.bindings[action_index].remove(binding_index);
|
||||
self.action_bindings[action_index].remove(binding_index);
|
||||
self.update_modifier_blocks();
|
||||
return;
|
||||
}
|
||||
if text_button(d, &globals.mouse, buttons_x + 85, y, 45, "edit") {
|
||||
self.editing_binding = Some((action, binding_index, BindingEdit::Init));
|
||||
}
|
||||
//
|
||||
let trigger = format!("{:?}", binding.trigger);
|
||||
d.draw_text(&trigger, binding_text_x, y + 5, 20, Color::LIMEGREEN);
|
||||
let x = binding_text_x + 10 + d.measure_text(&trigger, 20);
|
||||
let mut x = binding_text_x;
|
||||
d.draw_text(&trigger, x, y + 5, 20, Color::LIMEGREEN);
|
||||
x += 10 + d.measure_text(&trigger, 20);
|
||||
//
|
||||
let modifiers = format!("{:?}", binding.modifiers);
|
||||
d.draw_text(&modifiers, x, y + 5, 20, Color::LIGHTBLUE);
|
||||
let conflicts = conflicts(&self.bindings, binding, action);
|
||||
x += 10 + d.measure_text(&modifiers, 20);
|
||||
//
|
||||
let conflicts = conflicts(&self.action_bindings, binding, action);
|
||||
if !conflicts.is_empty() {
|
||||
let x = x + 10 + d.measure_text(&modifiers, 20);
|
||||
d.draw_text(
|
||||
&format!("also used by: {conflicts:?}"),
|
||||
x,
|
||||
y + 5,
|
||||
20,
|
||||
Color::ORANGERED,
|
||||
);
|
||||
let conflict_text = format!("also used by: {conflicts:?}");
|
||||
d.draw_text(&conflict_text, x, y + 5, 20, Color::ORANGERED);
|
||||
x += 10 + d.measure_text(&conflict_text, 20);
|
||||
}
|
||||
//
|
||||
if !binding.blocking_modifiers.is_empty() {
|
||||
let blocking_text = format!("not while: {:?}", binding.blocking_modifiers);
|
||||
d.draw_text(&blocking_text, x, y + 5, 20, Color::GRAY);
|
||||
}
|
||||
y += 32;
|
||||
}
|
||||
if text_button(d, &globals.mouse, buttons_x, y, 130, "add binding") {
|
||||
self.editing_binding =
|
||||
Some((action, self.bindings[action_index].len(), BindingEdit::Init));
|
||||
self.editing_binding = Some((
|
||||
action,
|
||||
self.action_bindings[action_index].len(),
|
||||
BindingEdit::Init,
|
||||
));
|
||||
}
|
||||
y += 45;
|
||||
}
|
||||
|
@ -193,6 +216,7 @@ impl Input {
|
|||
BindingEdit::Init => {
|
||||
if key.just_pressed(d) {
|
||||
*edit_state = BindingEdit::Adding(Binding {
|
||||
blocking_modifiers: Vec::new(),
|
||||
modifiers: Vec::new(),
|
||||
trigger: key,
|
||||
});
|
||||
|
@ -218,6 +242,7 @@ impl Input {
|
|||
globals.mouse.is_over(ok_btn_rect) && key == Button::MouseLeft;
|
||||
if key.just_pressed(d) && !clicking_ok {
|
||||
*edit_state = BindingEdit::Adding(Binding {
|
||||
blocking_modifiers: Vec::new(),
|
||||
modifiers: Vec::new(),
|
||||
trigger: key,
|
||||
});
|
||||
|
@ -235,7 +260,7 @@ impl Input {
|
|||
let text = format!("{:?} + {:?}", b.modifiers, b.trigger);
|
||||
d.draw_text(&text, x + 5, y + 5, 20, colour);
|
||||
|
||||
let conflicts = conflicts(&self.bindings, b, *action);
|
||||
let conflicts = conflicts(&self.action_bindings, b, *action);
|
||||
if !conflicts.is_empty() {
|
||||
d.draw_text(
|
||||
&format!("conflicts: {conflicts:?}"),
|
||||
|
@ -248,13 +273,14 @@ impl Input {
|
|||
}
|
||||
if text_button(d, &globals.mouse, ok_btn_x, ok_btn_y, ok_btn_width, "ok") {
|
||||
if let BindingEdit::Releasing(binding) = edit_state {
|
||||
let binding_list = &mut self.bindings[*action as usize];
|
||||
let binding_list = &mut self.action_bindings[*action as usize];
|
||||
if *binding_index < binding_list.len() {
|
||||
binding_list[*binding_index] = binding.clone();
|
||||
} else {
|
||||
binding_list.push(binding.clone());
|
||||
}
|
||||
self.editing_binding = None;
|
||||
self.update_modifier_blocks();
|
||||
}
|
||||
}
|
||||
if text_button(d, &globals.mouse, x + 100, y + 40, 80, "cancel") {
|
||||
|
@ -264,16 +290,10 @@ impl Input {
|
|||
}
|
||||
|
||||
pub fn update(&mut self, rl: &RaylibHandle) {
|
||||
for i in 0..ActionId::SIZE {
|
||||
let bindings = &self.bindings[i];
|
||||
let mut is_active = false;
|
||||
for binding in bindings {
|
||||
if binding.modifiers.iter().all(|&m| m.is_down(rl)) {
|
||||
is_active |= binding.trigger.is_down(rl);
|
||||
}
|
||||
}
|
||||
let state = &mut self.states[i];
|
||||
*state = if is_active {
|
||||
for i in 0..Button::SIZE {
|
||||
let button = Button::from_usize(i).unwrap();
|
||||
let state = &mut self.key_states[i];
|
||||
*state = if button.is_down(rl) {
|
||||
match state {
|
||||
BindingState::Off | BindingState::Released => BindingState::Pressed,
|
||||
BindingState::Pressed | BindingState::Held => BindingState::Held,
|
||||
|
@ -285,19 +305,67 @@ impl Input {
|
|||
}
|
||||
}
|
||||
}
|
||||
for i in 0..ActionId::SIZE {
|
||||
let bindings = &self.action_bindings[i];
|
||||
let mut is_active = BindingState::Off;
|
||||
if !self.in_text_edit {
|
||||
for binding in bindings {
|
||||
if binding
|
||||
.modifiers
|
||||
.iter()
|
||||
.all(|&m| self.key_states[m as usize] == BindingState::Held)
|
||||
&& binding
|
||||
.blocking_modifiers
|
||||
.iter()
|
||||
.all(|&m| self.key_states[m as usize] == BindingState::Off)
|
||||
{
|
||||
is_active = is_active.or(self.key_states[binding.trigger as usize]);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.action_states[i] = is_active;
|
||||
}
|
||||
self.in_text_edit = false;
|
||||
}
|
||||
|
||||
pub fn is_pressed(&self, action: ActionId) -> bool {
|
||||
self.states[action as usize] == BindingState::Pressed
|
||||
self.action_states[action as usize] == BindingState::Pressed
|
||||
}
|
||||
|
||||
pub fn is_held(&self, action: ActionId) -> bool {
|
||||
self.states[action as usize] == BindingState::Pressed
|
||||
|| self.states[action as usize] == BindingState::Held
|
||||
self.action_states[action as usize] == BindingState::Pressed
|
||||
|| self.action_states[action as usize] == BindingState::Held
|
||||
}
|
||||
|
||||
pub fn is_released(&self, action: ActionId) -> bool {
|
||||
self.states[action as usize] == BindingState::Released
|
||||
self.action_states[action as usize] == BindingState::Released
|
||||
}
|
||||
|
||||
/// Must be called after any binding has changed.
|
||||
/// Ensures a binding "S" is not triggered by "Ctrl+S", if "Ctrl+S" is bound to something else.
|
||||
pub fn update_modifier_blocks(&mut self) {
|
||||
for i in 0..ActionId::SIZE {
|
||||
let bindings = &self.action_bindings[i];
|
||||
for binding_index in 0..bindings.len() {
|
||||
let binding = &self.action_bindings[i][binding_index];
|
||||
let mut blocking_mods = Vec::new();
|
||||
for i in 0..ActionId::SIZE {
|
||||
let other_bindings = &self.action_bindings[i];
|
||||
for other_binding in other_bindings {
|
||||
if other_binding.trigger == binding.trigger {
|
||||
for modifier in &other_binding.modifiers {
|
||||
if !blocking_mods.contains(modifier)
|
||||
&& !binding.modifiers.contains(modifier)
|
||||
{
|
||||
blocking_mods.push(*modifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.action_bindings[i][binding_index].blocking_modifiers = blocking_mods;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -338,15 +406,38 @@ impl ActionId {
|
|||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Binding {
|
||||
#[serde(skip)]
|
||||
blocking_modifiers: Vec<Button>,
|
||||
modifiers: Vec<Button>,
|
||||
trigger: Button,
|
||||
}
|
||||
|
||||
impl BindingState {
|
||||
fn or(self, rhs: Self) -> Self {
|
||||
use BindingState::*;
|
||||
match (self, rhs) {
|
||||
(Off, other) => other,
|
||||
(other, Off) => other,
|
||||
(Held, _) => Held,
|
||||
(_, Held) => Held,
|
||||
(Pressed, Pressed) => Pressed,
|
||||
(Released, Released) => Released,
|
||||
(Pressed, Released) => Held,
|
||||
(Released, Pressed) => Held,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InputMap> for Input {
|
||||
fn from(value: InputMap) -> Self {
|
||||
fn from(map: InputMap) -> Self {
|
||||
let mut new = Self::default();
|
||||
for (action, loaded_bindings) in value {
|
||||
new.bindings[action as usize] = loaded_bindings;
|
||||
for (action, loaded_bindings) in map {
|
||||
let temp_json = format!("\"{action}\"");
|
||||
if let Ok(action) = serde_json::from_str::<ActionId>(&temp_json) {
|
||||
new.action_bindings[action as usize] = loaded_bindings;
|
||||
} else {
|
||||
println!("'{action}' is not a valid action id, bindings discarded");
|
||||
}
|
||||
}
|
||||
new
|
||||
}
|
||||
|
@ -355,11 +446,14 @@ impl From<InputMap> for Input {
|
|||
impl From<Input> for InputMap {
|
||||
fn from(value: Input) -> Self {
|
||||
value
|
||||
.bindings
|
||||
.action_bindings
|
||||
.iter()
|
||||
.enumerate()
|
||||
// for this to panic, the .bindings array would have to be larger than ActionId::SIZE
|
||||
.map(|(i, b)| (ActionId::from_usize(i).unwrap(), b.clone()))
|
||||
.map(|(i, b)| {
|
||||
// for this to panic, the .bindings array would have to be larger than ActionId::SIZE
|
||||
let action = ActionId::from_usize(i).unwrap();
|
||||
(format!("{action:?}"), b.clone())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
20
src/level.rs
20
src/level.rs
|
@ -6,6 +6,8 @@ use crate::board::Board;
|
|||
pub struct Chapter {
|
||||
pub title: String,
|
||||
pub levels: Vec<Level>,
|
||||
#[serde(default = "default_true")]
|
||||
pub visible: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
|
@ -47,6 +49,20 @@ impl IOData {
|
|||
}
|
||||
|
||||
impl Level {
|
||||
pub fn new_orphan(id: &str) -> Self {
|
||||
Self {
|
||||
id: id.to_owned(),
|
||||
name: id.to_owned(),
|
||||
description: String::from(
|
||||
"No level with this id was found, but there are saved solutions pointing to it.\n
|
||||
Because input values and expected output is not available, this functions as a sandbox.\n
|
||||
This allows you to recover any machines you have built here.",
|
||||
),
|
||||
init_board: None,
|
||||
stages: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
@ -81,3 +97,7 @@ impl Stage {
|
|||
&self.output
|
||||
}
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
|
|
@ -36,10 +36,11 @@ impl Globals {
|
|||
textures.load_dir("assets/digits", rl, thread);
|
||||
|
||||
let config_path = userdata_dir().join(CONFIG_FILE_NAME);
|
||||
let config = fs::read_to_string(config_path)
|
||||
let mut config: Config = fs::read_to_string(config_path)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
.unwrap_or_default();
|
||||
config.input.update_modifier_blocks();
|
||||
|
||||
Self {
|
||||
clipboard: Clipboard::new()
|
||||
|
|
215
src/main.rs
215
src/main.rs
|
@ -1,6 +1,6 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{read_dir, read_to_string, File},
|
||||
fs::{create_dir_all, read_dir, read_to_string, File},
|
||||
io::Write,
|
||||
};
|
||||
|
||||
|
@ -19,11 +19,11 @@ use util::*;
|
|||
const TITLE_TEXT: &str = concat!("Marble Machinations v", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
struct Game {
|
||||
levels: Vec<LevelListEntry>,
|
||||
level_scroll: usize,
|
||||
chapters: Vec<Chapter>,
|
||||
level_scroll: i32,
|
||||
solutions: HashMap<String, Vec<Solution>>,
|
||||
open_editor: Option<Editor>,
|
||||
selected_level: usize,
|
||||
selected_level: (usize, usize),
|
||||
selected_solution: usize,
|
||||
delete_solution: Option<usize>,
|
||||
editing_solution_name: bool,
|
||||
|
@ -32,12 +32,6 @@ struct Game {
|
|||
edit_settings: Option<Config>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum LevelListEntry {
|
||||
Level(Level),
|
||||
Chapter(String, usize),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let (mut rl, thread) = raylib::init().resizable().title(TITLE_TEXT).build();
|
||||
rl.set_target_fps(60);
|
||||
|
@ -52,15 +46,15 @@ fn main() {
|
|||
|
||||
impl Game {
|
||||
fn new(rl: &mut RaylibHandle, thread: &RaylibThread) -> Self {
|
||||
let levels = get_levels();
|
||||
let solutions = get_solutions();
|
||||
let chapters = get_chapters(&solutions);
|
||||
|
||||
Self {
|
||||
levels,
|
||||
chapters,
|
||||
level_scroll: 0,
|
||||
solutions,
|
||||
open_editor: None,
|
||||
selected_level: 0,
|
||||
selected_level: (0, 0),
|
||||
selected_solution: 0,
|
||||
delete_solution: None,
|
||||
editing_solution_name: false,
|
||||
|
@ -147,71 +141,89 @@ impl Game {
|
|||
|
||||
const ENTRY_SPACING: i32 = 65;
|
||||
let fit_on_screen = (d.get_screen_height() / ENTRY_SPACING) as usize;
|
||||
let max_scroll = self.levels.len().saturating_sub(fit_on_screen);
|
||||
let max_scroll = self
|
||||
.chapters
|
||||
.iter()
|
||||
.map(|c| 1 + if c.visible { c.levels.len() } else { 0 })
|
||||
.sum::<usize>()
|
||||
.saturating_sub(fit_on_screen) as i32
|
||||
* ENTRY_SPACING;
|
||||
if self.globals.mouse.pos().x < level_list_width as f32 {
|
||||
if self.globals.mouse.scroll() == Some(Scroll::Down) && self.level_scroll < max_scroll {
|
||||
self.level_scroll += 1;
|
||||
self.level_scroll += ENTRY_SPACING;
|
||||
}
|
||||
if self.globals.mouse.scroll() == Some(Scroll::Up) && self.level_scroll > 0 {
|
||||
self.level_scroll -= 1;
|
||||
self.level_scroll -= ENTRY_SPACING;
|
||||
}
|
||||
}
|
||||
|
||||
for (row_index, level_index) in (self.level_scroll..self.levels.len()).enumerate() {
|
||||
let level = &mut self.levels[level_index];
|
||||
let y = 10 + row_index as i32 * ENTRY_SPACING;
|
||||
let bounds = Rectangle {
|
||||
x: 5.,
|
||||
y: y as f32 - 5.,
|
||||
width: level_list_width as f32 - 10.,
|
||||
height: ENTRY_SPACING as f32 - 5.,
|
||||
};
|
||||
let mut y = 10 - self.level_scroll;
|
||||
for (chapter_i, chapter) in self.chapters.iter_mut().enumerate() {
|
||||
let bounds = rect(5, y - 5, level_list_width - 10, ENTRY_SPACING - 5);
|
||||
d.draw_rectangle_rec(bounds, BG_DARK);
|
||||
d.draw_text(&chapter.title, 10, y, 30, FG_CHAPTER_TITLE);
|
||||
let subtitle = format!("{} levels", chapter.levels.len());
|
||||
d.draw_text(&subtitle, 10, y + 30, 20, Color::WHITE);
|
||||
y += ENTRY_SPACING;
|
||||
let clicked_this =
|
||||
self.globals.mouse.left_click() && self.globals.mouse.is_over(bounds);
|
||||
match level {
|
||||
LevelListEntry::Chapter(title, level_count) => {
|
||||
d.draw_rectangle_rec(bounds, BG_DARK);
|
||||
d.draw_text(title, 10, y, 30, FG_CHAPTER_TITLE);
|
||||
let subtitle = format!("{level_count} levels");
|
||||
d.draw_text(&subtitle, 10, y + 30, 20, Color::WHITE);
|
||||
}
|
||||
LevelListEntry::Level(level) => {
|
||||
if clicked_this && self.selected_level != level_index {
|
||||
self.editing_solution_name = false;
|
||||
self.selected_level = level_index;
|
||||
self.selected_solution = 0;
|
||||
self.delete_solution = None;
|
||||
// select the last solution of the level, if there is one
|
||||
if let Some(solutions) = self.solutions.get(level.id()) {
|
||||
self.selected_solution = solutions.len().saturating_sub(1);
|
||||
}
|
||||
}
|
||||
d.draw_rectangle_rec(bounds, widget_bg(self.selected_level == level_index));
|
||||
|
||||
let mut title_color = Color::WHITE;
|
||||
if clicked_this {
|
||||
chapter.visible = !chapter.visible;
|
||||
}
|
||||
|
||||
if !chapter.visible {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (level_index, level) in chapter.levels.iter().enumerate() {
|
||||
let bounds = rect(5, y - 5, level_list_width - 10, ENTRY_SPACING - 5);
|
||||
let clicked_this =
|
||||
self.globals.mouse.left_click() && self.globals.mouse.is_over(bounds);
|
||||
|
||||
if clicked_this && self.selected_level != (chapter_i, level_index) {
|
||||
self.editing_solution_name = false;
|
||||
self.selected_level = (chapter_i, level_index);
|
||||
self.selected_solution = 0;
|
||||
self.delete_solution = None;
|
||||
// select the last solution of the level, if there is one
|
||||
if let Some(solutions) = self.solutions.get(level.id()) {
|
||||
if solutions.iter().any(|s| s.score.is_some()) {
|
||||
title_color = Color::LIGHTGREEN;
|
||||
}
|
||||
self.selected_solution = solutions.len().saturating_sub(1);
|
||||
}
|
||||
d.draw_text(level.name(), 10, y, 30, title_color);
|
||||
let solution_count = self
|
||||
.solutions
|
||||
.get(level.id())
|
||||
.map(Vec::len)
|
||||
.unwrap_or_default();
|
||||
let subtext = format!("solutions: {solution_count}");
|
||||
let subtext_color = if solution_count > 0 {
|
||||
Color::GOLD
|
||||
} else {
|
||||
Color::LIGHTGRAY
|
||||
};
|
||||
d.draw_text(&subtext, 10, y + 30, 20, subtext_color);
|
||||
}
|
||||
d.draw_rectangle_rec(
|
||||
bounds,
|
||||
widget_bg(self.selected_level == (chapter_i, level_index)),
|
||||
);
|
||||
|
||||
let mut title_color = Color::WHITE;
|
||||
if let Some(solutions) = self.solutions.get(level.id()) {
|
||||
if solutions.iter().any(|s| s.score.is_some()) {
|
||||
title_color = Color::LIGHTGREEN;
|
||||
}
|
||||
}
|
||||
d.draw_text(level.name(), 10, y, 30, title_color);
|
||||
let solution_count = self
|
||||
.solutions
|
||||
.get(level.id())
|
||||
.map(Vec::len)
|
||||
.unwrap_or_default();
|
||||
let subtext = format!("solutions: {solution_count}");
|
||||
let subtext_color = if solution_count > 0 {
|
||||
Color::GOLD
|
||||
} else {
|
||||
Color::LIGHTGRAY
|
||||
};
|
||||
d.draw_text(&subtext, 10, y + 30, 20, subtext_color);
|
||||
y += ENTRY_SPACING;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(LevelListEntry::Level(level)) = self.levels.get(self.selected_level) {
|
||||
if let Some(level) = self
|
||||
.chapters
|
||||
.get(self.selected_level.0)
|
||||
.and_then(|c| c.levels.get(self.selected_level.1))
|
||||
{
|
||||
d.draw_text(level.name(), level_list_width + 10, 10, 40, Color::CYAN);
|
||||
d.draw_text(level.id(), level_list_width + 10, 50, 10, Color::GRAY);
|
||||
|
||||
|
@ -303,7 +315,7 @@ impl Game {
|
|||
let bounds = Rectangle::new(column_x as f32, y as f32, 220., 30.);
|
||||
if text_input(
|
||||
d,
|
||||
&self.globals.mouse,
|
||||
&mut self.globals,
|
||||
bounds,
|
||||
&mut solution.name,
|
||||
&mut self.editing_solution_name,
|
||||
|
@ -336,30 +348,41 @@ impl Game {
|
|||
}
|
||||
|
||||
fn save_config(&self) {
|
||||
_ = create_dir_all(userdata_dir());
|
||||
let path = userdata_dir().join(CONFIG_FILE_NAME);
|
||||
let json = serde_json::to_string_pretty(&self.globals.config).unwrap();
|
||||
let mut f = File::create(path).unwrap();
|
||||
f.write_all(json.as_bytes()).unwrap();
|
||||
match File::create(path) {
|
||||
Ok(mut f) => {
|
||||
_ = f.write_all(json.as_bytes());
|
||||
}
|
||||
Err(e) => println!("error saving config: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_levels() -> Vec<LevelListEntry> {
|
||||
fn get_chapters(solutions: &HashMap<String, Vec<Solution>>) -> Vec<Chapter> {
|
||||
let mut chapters = Vec::<Chapter>::new();
|
||||
for d in read_dir("levels").unwrap().flatten() {
|
||||
if let Ok(text) = read_to_string(d.path()) {
|
||||
if let Ok(chapter) = serde_json::from_str(&text) {
|
||||
if let Ok(mut chapter) = serde_json::from_str::<Chapter>(&text) {
|
||||
chapter.visible = false;
|
||||
for level in &chapter.levels {
|
||||
if solutions
|
||||
.get(level.id())
|
||||
.map(|s| s.iter().any(|s| s.score.is_some()))
|
||||
!= Some(true)
|
||||
{
|
||||
// only expand chapters where at least one level has no complete solutions
|
||||
chapter.visible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
chapters.push(chapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
chapters.sort_unstable_by_key(|c| c.title.clone());
|
||||
|
||||
let mut level_list = Vec::new();
|
||||
for c in chapters {
|
||||
level_list.push(LevelListEntry::Chapter(c.title, c.levels.len()));
|
||||
level_list.extend(c.levels.into_iter().map(LevelListEntry::Level));
|
||||
}
|
||||
|
||||
// user levels
|
||||
let mut custom_levels = Vec::new();
|
||||
let custom_level_dir = userdata_dir().join("levels");
|
||||
|
@ -372,15 +395,15 @@ fn get_levels() -> Vec<LevelListEntry> {
|
|||
}
|
||||
}
|
||||
}
|
||||
level_list.push(LevelListEntry::Chapter(
|
||||
"Custom levels".into(),
|
||||
custom_levels.len(),
|
||||
));
|
||||
for l in custom_levels {
|
||||
level_list.push(LevelListEntry::Level(l));
|
||||
chapters.push(Chapter {
|
||||
title: "Custom levels".into(),
|
||||
levels: custom_levels,
|
||||
visible: true,
|
||||
});
|
||||
if let Some(orphans) = find_orphans(&chapters, solutions) {
|
||||
chapters.push(orphans);
|
||||
}
|
||||
|
||||
level_list
|
||||
chapters
|
||||
}
|
||||
|
||||
fn get_solutions() -> HashMap<String, Vec<Solution>> {
|
||||
|
@ -403,9 +426,37 @@ fn get_solutions() -> HashMap<String, Vec<Solution>> {
|
|||
}
|
||||
solutions.sort_unstable_by_key(Solution::id);
|
||||
}
|
||||
by_level.insert(level_name, solutions);
|
||||
if !solutions.is_empty() {
|
||||
by_level.insert(level_name, solutions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
by_level
|
||||
}
|
||||
|
||||
fn find_orphans(
|
||||
chapters: &[Chapter],
|
||||
solutions: &HashMap<String, Vec<Solution>>,
|
||||
) -> Option<Chapter> {
|
||||
let mut orphan_levels = Vec::new();
|
||||
'outer: for id in solutions.keys() {
|
||||
for c in chapters {
|
||||
for l in &c.levels {
|
||||
if l.id() == id {
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
orphan_levels.push(Level::new_orphan(id))
|
||||
}
|
||||
if orphan_levels.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Chapter {
|
||||
title: "Missing levels".into(),
|
||||
levels: orphan_levels,
|
||||
visible: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,13 +12,19 @@ use tile::*;
|
|||
pub struct Machine {
|
||||
grid: Grid,
|
||||
marbles: Vec<Pos>,
|
||||
powered: Vec<Pos>,
|
||||
input: Vec<u8>,
|
||||
input_index: usize,
|
||||
output: Vec<u8>,
|
||||
steps: usize,
|
||||
pub subtick_index: usize,
|
||||
pub debug_subticks: Vec<DebugSubTick>,
|
||||
// used across steps
|
||||
powered: Vec<Pos>,
|
||||
// used within steps
|
||||
new_marbles: Vec<(Pos, MarbleValue, Direction, bool)>,
|
||||
influenced_direction: Vec<(bool, DirInfluence)>,
|
||||
claim_positions: Vec<Pos>,
|
||||
removed_marbles: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -27,18 +33,29 @@ pub struct DebugSubTick {
|
|||
pub pos: Option<Pos>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum DirInfluence {
|
||||
None,
|
||||
One(Direction),
|
||||
Multiple,
|
||||
}
|
||||
|
||||
impl Machine {
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
grid: Grid::new_empty(5, 5),
|
||||
marbles: Vec::new(),
|
||||
powered: Vec::new(),
|
||||
input: Vec::new(),
|
||||
input_index: 0,
|
||||
output: Vec::new(),
|
||||
steps: 0,
|
||||
subtick_index: 0,
|
||||
debug_subticks: Vec::new(),
|
||||
powered: Vec::new(),
|
||||
new_marbles: Vec::new(),
|
||||
influenced_direction: Vec::new(),
|
||||
claim_positions: Vec::new(),
|
||||
removed_marbles: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,9 +136,51 @@ impl Machine {
|
|||
});
|
||||
}
|
||||
|
||||
self.step_power_complete();
|
||||
|
||||
if self.marbles.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.step_find_bounces();
|
||||
self.step_apply_direct_bounces();
|
||||
|
||||
self.step_prepare_creating_marbles();
|
||||
let old_marbles = self.marbles.len();
|
||||
|
||||
let mut new_marbles = Vec::new();
|
||||
self.step_create_marbles();
|
||||
|
||||
// #### movement ####
|
||||
// mark claims to figure out what spaces can be moved to
|
||||
self.step_mark_claims(old_marbles);
|
||||
self.step_move_marbles(old_marbles);
|
||||
|
||||
for pos in self.claim_positions.drain(..) {
|
||||
if let Some(Tile::Open(_, claim_state)) = self.grid.get_mut(pos) {
|
||||
*claim_state = Claim::Free;
|
||||
}
|
||||
}
|
||||
|
||||
// remove marbles
|
||||
for &i in self.removed_marbles.iter().rev() {
|
||||
self.grid.set(self.marbles[i], Tile::BLANK);
|
||||
self.marbles.swap_remove(i);
|
||||
}
|
||||
|
||||
self.step_propagate_power();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.debug_subticks.push(DebugSubTick {
|
||||
grid: self.grid.clone(),
|
||||
pos: None,
|
||||
});
|
||||
self.subtick_index = self.debug_subticks.len() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
|
||||
fn step_power_complete(&mut self) {
|
||||
// activate all powered machines
|
||||
for &pos in &self.powered {
|
||||
match self.grid.get_mut(pos) {
|
||||
|
@ -157,20 +216,19 @@ impl Machine {
|
|||
MathOp::Div => val_a.checked_div(val_b).unwrap_or_default(),
|
||||
MathOp::Rem => val_a.checked_rem(val_b).unwrap_or_default(),
|
||||
};
|
||||
new_marbles.push((front_pos, value, dir));
|
||||
self.new_marbles.push((front_pos, value, dir, false));
|
||||
}
|
||||
}
|
||||
PTile::IO => {
|
||||
if front_tile == &Tile::BLANK && self.input_index < self.input.len()
|
||||
{
|
||||
let value = self.input[self.input_index] as MarbleValue;
|
||||
self.input_index += 1;
|
||||
new_marbles.push((front_pos, value, dir));
|
||||
self.new_marbles.push((front_pos, value, dir, true));
|
||||
}
|
||||
}
|
||||
PTile::Silo => {
|
||||
if front_tile == &Tile::BLANK {
|
||||
new_marbles.push((front_pos, 0, dir));
|
||||
self.new_marbles.push((front_pos, 0, dir, false));
|
||||
}
|
||||
}
|
||||
PTile::Flipper => {
|
||||
|
@ -198,21 +256,13 @@ impl Machine {
|
|||
};
|
||||
}
|
||||
self.powered.clear();
|
||||
}
|
||||
|
||||
if self.marbles.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum DirInfluence {
|
||||
None,
|
||||
One(Direction),
|
||||
Multiple,
|
||||
}
|
||||
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
|
||||
fn step_find_bounces(&mut self) {
|
||||
// #### find all direct bounces ####
|
||||
let mut will_reverse_direction = vec![false; self.marbles.len()];
|
||||
// todo store in tile to remove search through self.marbles
|
||||
let mut influenced_direction = vec![DirInfluence::None; self.marbles.len()];
|
||||
self.influenced_direction = vec![(false, DirInfluence::None); self.marbles.len()];
|
||||
|
||||
for (i, &pos) in self.marbles.iter().enumerate() {
|
||||
let Some(Tile::Marble { value: _, dir }) = self.grid.get(pos) else {
|
||||
|
@ -229,11 +279,11 @@ impl Machine {
|
|||
} => {
|
||||
if other_dir != dir {
|
||||
// this marble is facing another marble, and will therefore definitely bounce
|
||||
will_reverse_direction[i] = true;
|
||||
self.influenced_direction[i].0 = true;
|
||||
// the other marble will bounce too, either
|
||||
let other_index =
|
||||
self.marbles.iter().position(|m| *m == front_pos).unwrap();
|
||||
let influence = &mut influenced_direction[other_index];
|
||||
let influence = &mut self.influenced_direction[other_index].1;
|
||||
*influence = match *influence {
|
||||
DirInfluence::None => DirInfluence::One(dir),
|
||||
DirInfluence::One(_) => DirInfluence::Multiple,
|
||||
|
@ -244,48 +294,65 @@ impl Machine {
|
|||
Tile::Arrow(arrow_dir) => {
|
||||
if arrow_dir == dir.opposite() {
|
||||
// bounce on a reverse facing arrow
|
||||
will_reverse_direction[i] = true;
|
||||
self.influenced_direction[i].0 = true;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
|
||||
fn step_apply_direct_bounces(&mut self) {
|
||||
// #### apply all direct bounces ####
|
||||
for (i, &pos) in self.marbles.iter().enumerate() {
|
||||
let Some(Tile::Marble { value: _, dir }) = self.grid.get_mut(pos) else {
|
||||
unreachable!()
|
||||
};
|
||||
if will_reverse_direction[i] {
|
||||
if self.influenced_direction[i].0 {
|
||||
*dir = dir.opposite();
|
||||
} else if let DirInfluence::One(new_dir) = influenced_direction[i] {
|
||||
} else if let DirInfluence::One(new_dir) = self.influenced_direction[i].1 {
|
||||
*dir = new_dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
|
||||
fn step_prepare_creating_marbles(&mut self) {
|
||||
// #### new marbles ####
|
||||
let mut claim_positions = Vec::new();
|
||||
// prepare creating the new marbles
|
||||
for &(pos, _val, _dir) in &new_marbles {
|
||||
self.claim_positions.clear(); // already drained
|
||||
// prepare creating the new marbles
|
||||
for &(pos, _val, _dir, _is_input) in &self.new_marbles {
|
||||
let Some(Tile::Open(OpenTile::Blank, claim)) = self.grid.get_mut(pos) else {
|
||||
unreachable!()
|
||||
};
|
||||
if claim.claim_indirect() {
|
||||
claim_positions.push(pos);
|
||||
self.claim_positions.push(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
|
||||
fn step_create_marbles(&mut self) {
|
||||
// create new marbles
|
||||
let mut advance_input = false;
|
||||
// new marbles are past old_marbles index, so will not move this step
|
||||
for (pos, value, dir) in new_marbles {
|
||||
for (pos, value, dir, is_input) in self.new_marbles.drain(..) {
|
||||
let Some(Tile::Open(OpenTile::Blank, Claim::ClaimedIndirect)) = self.grid.get_mut(pos)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
advance_input |= is_input;
|
||||
self.grid.set(pos, Tile::Marble { value, dir });
|
||||
self.marbles.push(pos);
|
||||
}
|
||||
if advance_input {
|
||||
self.input_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// #### movement ####
|
||||
// mark claims to figure out what spaces can be moved to
|
||||
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
|
||||
fn step_mark_claims(&mut self, old_marbles: usize) {
|
||||
for &pos in &self.marbles[..old_marbles] {
|
||||
let Some(Tile::Marble { value: _, dir }) = self.grid.get(pos) else {
|
||||
unreachable!()
|
||||
|
@ -296,7 +363,7 @@ impl Machine {
|
|||
};
|
||||
if let Tile::Open(_type, claim) = front_tile {
|
||||
if claim.claim() {
|
||||
claim_positions.push(front_pos);
|
||||
self.claim_positions.push(front_pos);
|
||||
}
|
||||
} else {
|
||||
let target_pos = match front_tile {
|
||||
|
@ -310,13 +377,16 @@ impl Machine {
|
|||
};
|
||||
if let Tile::Open(_type, claim) = target_tile {
|
||||
if claim.claim_indirect() {
|
||||
claim_positions.push(front_pos);
|
||||
self.claim_positions.push(front_pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut removed_marbles = Vec::new();
|
||||
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
|
||||
fn step_move_marbles(&mut self, old_marbles: usize) {
|
||||
self.removed_marbles.clear();
|
||||
// move marbles
|
||||
for (i, pos) in self.marbles[..old_marbles].iter_mut().enumerate() {
|
||||
let Some(Tile::Marble { value, dir }) = self.grid.get(*pos) else {
|
||||
|
@ -369,11 +439,11 @@ impl Machine {
|
|||
target_pos = dir.step(front_pos);
|
||||
}
|
||||
Tile::Powerable(PTile::Silo, _) => {
|
||||
removed_marbles.push(i);
|
||||
self.removed_marbles.push(i);
|
||||
continue;
|
||||
}
|
||||
Tile::Powerable(PTile::IO, _) => {
|
||||
removed_marbles.push(i);
|
||||
self.removed_marbles.push(i);
|
||||
self.output.push(value as u8);
|
||||
continue;
|
||||
}
|
||||
|
@ -390,19 +460,10 @@ impl Machine {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for pos in claim_positions {
|
||||
if let Some(Tile::Open(_, claim_state)) = self.grid.get_mut(pos) {
|
||||
*claim_state = Claim::Free;
|
||||
}
|
||||
}
|
||||
|
||||
// remove marbles
|
||||
for &i in removed_marbles.iter().rev() {
|
||||
self.grid.set(self.marbles[i], Tile::BLANK);
|
||||
self.marbles.swap_remove(i);
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
|
||||
fn step_propagate_power(&mut self) {
|
||||
// propagate power
|
||||
let mut i = 0;
|
||||
while i < self.powered.len() {
|
||||
|
@ -527,13 +588,5 @@ impl Machine {
|
|||
}
|
||||
i += 1;
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.debug_subticks.push(DebugSubTick {
|
||||
grid: self.grid.clone(),
|
||||
pos: None,
|
||||
});
|
||||
self.subtick_index = self.debug_subticks.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -124,9 +124,9 @@ impl Grid {
|
|||
}
|
||||
|
||||
pub fn used_bounds_area(&self) -> usize {
|
||||
let row_clear = |a, max, f: fn(usize, usize) -> (usize, usize)| {
|
||||
for b in 0..max {
|
||||
if !self.get_unchecked(f(a, b).into()).is_blank() {
|
||||
let row_clear = |y| {
|
||||
for x in 0..self.width {
|
||||
if !self.get_unchecked((x, y).into()).is_blank() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -134,29 +134,37 @@ impl Grid {
|
|||
};
|
||||
let mut height = self.height;
|
||||
for y in 0..self.height {
|
||||
if row_clear(y, self.width, |y, x| (x, y)) {
|
||||
if row_clear(y) {
|
||||
height -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for y in (0..self.height).rev() {
|
||||
if row_clear(y, self.width, |y, x| (x, y)) {
|
||||
if row_clear(y) {
|
||||
height -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let col_clear = |x| {
|
||||
for y in 0..self.height {
|
||||
if !self.get_unchecked((x, y).into()).is_blank() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
};
|
||||
let mut width = self.width;
|
||||
for x in 0..self.width {
|
||||
if row_clear(x, self.height, |x, y| (x, y)) {
|
||||
if col_clear(x) {
|
||||
width -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for x in (0..self.width).rev() {
|
||||
if row_clear(x, self.width, |x, y| (x, y)) {
|
||||
if col_clear(x) {
|
||||
width -= 1;
|
||||
} else {
|
||||
break;
|
||||
|
@ -277,16 +285,23 @@ impl Grid {
|
|||
out
|
||||
}
|
||||
|
||||
pub fn draw(&self, d: &mut RaylibDrawHandle, textures: &Textures, offset: Vector2, scale: f32) {
|
||||
pub fn draw(
|
||||
&self,
|
||||
d: &mut RaylibDrawHandle,
|
||||
textures: &Textures,
|
||||
offset: Vector2,
|
||||
scale: f32,
|
||||
power_directions: bool,
|
||||
) {
|
||||
let tile_size = (TILE_TEXTURE_SIZE * scale) as i32;
|
||||
|
||||
let start_x = (-offset.x as i32) / tile_size - 1;
|
||||
let tile_width = d.get_screen_width() / tile_size + 2;
|
||||
let tiles_width = d.get_screen_width() / tile_size + 3;
|
||||
let start_y = (-offset.y as i32) / tile_size - 1;
|
||||
let tile_height = d.get_screen_height() / tile_size + 2;
|
||||
let tiles_height = d.get_screen_height() / tile_size + 3;
|
||||
|
||||
for x in start_x..(start_x + tile_width) {
|
||||
for y in start_y..(start_y + tile_height) {
|
||||
for x in start_x..(start_x + tiles_width) {
|
||||
for y in start_y..(start_y + tiles_height) {
|
||||
let px = x * tile_size + offset.x as i32;
|
||||
let py = y * tile_size + offset.y as i32;
|
||||
if let Some(tile) = self.get((x, y).into()) {
|
||||
|
@ -296,13 +311,13 @@ impl Grid {
|
|||
}
|
||||
let texture = textures.get(texname);
|
||||
draw_scaled_texture(d, texture, px, py, scale);
|
||||
#[cfg(debug_assertions)]
|
||||
// todo some in-game option to show power direction
|
||||
if let Tile::Powerable(_, state) = &tile {
|
||||
for dir in Direction::ALL {
|
||||
if state.get_dir(dir) {
|
||||
let texture = textures.get(dir.debug_arrow_texture_name());
|
||||
draw_scaled_texture(d, texture, px, py, scale);
|
||||
if power_directions {
|
||||
if let Tile::Powerable(_, state) = &tile {
|
||||
for dir in Direction::ALL {
|
||||
if state.get_dir(dir) {
|
||||
let texture = textures.get(dir.debug_arrow_texture_name());
|
||||
draw_scaled_texture(d, texture, px, py, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ pub struct Solution {
|
|||
pub score: Option<Score>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Score {
|
||||
pub cycles: usize,
|
||||
pub tiles: usize,
|
||||
|
|
49
src/ui.rs
49
src/ui.rs
|
@ -1,6 +1,10 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use crate::{theme::*, util::draw_scaled_texture, util::MouseInput, util::Scroll, util::Textures};
|
||||
use crate::{
|
||||
theme::*,
|
||||
util::{draw_scaled_texture, rect, MouseInput, Scroll, Textures},
|
||||
Globals,
|
||||
};
|
||||
use raylib::prelude::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -156,6 +160,35 @@ impl Tooltip {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn toggle_button(
|
||||
(d, mouse): (&mut RaylibDrawHandle, &MouseInput),
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
val: &mut bool,
|
||||
) {
|
||||
let margin = 5;
|
||||
let mouse_pos = mouse.pos();
|
||||
let bounds = rect(x, y, width, height);
|
||||
|
||||
let hover = bounds.check_collision_point_rec(mouse_pos);
|
||||
d.draw_rectangle(x, y, width, height, widget_bg(hover));
|
||||
let pressed = hover && mouse.left_click();
|
||||
if pressed {
|
||||
*val = !*val;
|
||||
}
|
||||
if *val {
|
||||
d.draw_rectangle(
|
||||
x + margin,
|
||||
y + margin,
|
||||
width - margin * 2,
|
||||
height - margin * 2,
|
||||
Color::WHITE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simple_button(
|
||||
(d, mouse): (&mut RaylibDrawHandle, &MouseInput),
|
||||
x: i32,
|
||||
|
@ -164,12 +197,7 @@ pub fn simple_button(
|
|||
height: i32,
|
||||
) -> bool {
|
||||
let mouse_pos = mouse.pos();
|
||||
let bounds = Rectangle {
|
||||
x: x as f32,
|
||||
y: y as f32,
|
||||
width: width as f32,
|
||||
height: height as f32,
|
||||
};
|
||||
let bounds = rect(x, y, width, height);
|
||||
let hover = bounds.check_collision_point_rec(mouse_pos);
|
||||
let pressed = hover && mouse.left_click();
|
||||
d.draw_rectangle(x, y, width, height, widget_bg(hover));
|
||||
|
@ -225,7 +253,7 @@ where
|
|||
|
||||
pub fn text_input(
|
||||
d: &mut RaylibDrawHandle,
|
||||
mouse: &MouseInput,
|
||||
globals: &mut Globals,
|
||||
bounds: Rectangle,
|
||||
text: &mut String,
|
||||
is_selected: &mut bool,
|
||||
|
@ -262,12 +290,13 @@ pub fn text_input(
|
|||
Color::WHITE,
|
||||
);
|
||||
};
|
||||
if editable && mouse.left_click() && (mouse.is_over(bounds) || *is_selected) {
|
||||
if editable && globals.mouse.left_click() && (globals.mouse.is_over(bounds) || *is_selected) {
|
||||
*is_selected = !*is_selected;
|
||||
}
|
||||
|
||||
if *is_selected {
|
||||
if d.is_key_pressed(KeyboardKey::KEY_ESCAPE) {
|
||||
globals.config.input.in_text_edit = true;
|
||||
if d.is_key_pressed(KeyboardKey::KEY_ESCAPE) || d.is_key_pressed(KeyboardKey::KEY_ENTER) {
|
||||
*is_selected = false;
|
||||
}
|
||||
if d.is_key_pressed(KeyboardKey::KEY_BACKSPACE) && !text.is_empty() {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use marble_machinations::marble_engine::{grid::Grid, Machine};
|
||||
|
||||
fn no_input_test(steps: usize, output: &[u8], grid: &str) {
|
||||
fn do_test(steps: usize, input: &[u8], output: &[u8], grid: &str) {
|
||||
let mut engine = Machine::new_empty();
|
||||
engine.set_grid(Grid::from_ascii(grid));
|
||||
engine.set_input(input.to_owned());
|
||||
for _ in 0..(steps - 1) {
|
||||
engine.step();
|
||||
}
|
||||
|
@ -11,11 +12,25 @@ fn no_input_test(steps: usize, output: &[u8], grid: &str) {
|
|||
assert_eq!(engine.output(), output, "expected output");
|
||||
}
|
||||
|
||||
fn no_input_test(steps: usize, output: &[u8], grid: &str) {
|
||||
do_test(steps, &[], output, grid)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creating_marbles_cause_indirect_claim() {
|
||||
no_input_test(3, &[1], " o \n|-*-|\n| 1 |\n-B B-\n I\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bug_input_consumed_when_marble_creation_blocked() {
|
||||
do_test(
|
||||
4,
|
||||
&[1, 2, 3],
|
||||
&[1, 1],
|
||||
"# +-+\nIIo| I\n *+B\nII\n++*\n# #\n",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bug_overlapping_marble_creation_blocks_tile_forever() {
|
||||
no_input_test(
|
||||
|
|
Loading…
Add table
Reference in a new issue