From 2517a9d33b7602463aba2e0591fdc9f18970277a Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Fri, 27 Feb 2026 02:58:35 +0100 Subject: [PATCH] complete rework of the user interface (using raygui) --- CMakeLists.txt | 1 + include/camera.hpp | 45 +-- include/config.hpp | 23 +- include/gui.hpp | 190 +++++++++++ include/input.hpp | 143 +++++++- include/octree.hpp | 2 - include/physics.hpp | 8 +- include/puzzle.hpp | 30 +- include/renderer.hpp | 13 +- include/state.hpp | 22 +- src/camera.cpp | 100 ++---- src/distance.cpp | 8 - src/gui.cpp | 792 +++++++++++++++++++++++++++++++++++++++++++ src/input.cpp | 622 ++++++++++++++++++++++++--------- src/main.cpp | 35 +- src/octree.cpp | 10 - src/physics.cpp | 24 +- src/puzzle.cpp | 22 +- src/renderer.cpp | 216 ++---------- src/state.cpp | 61 ++-- 20 files changed, 1781 insertions(+), 586 deletions(-) create mode 100644 include/gui.hpp create mode 100644 src/gui.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f826316..1309df7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ set(SOURCES src/tracy.cpp src/backward.cpp src/distance.cpp + src/gui.cpp ) # Main target diff --git a/include/camera.hpp b/include/camera.hpp index 5be4508..871aba9 100644 --- a/include/camera.hpp +++ b/include/camera.hpp @@ -7,43 +7,24 @@ #include class OrbitCamera3D { - friend class Renderer; +public: + Vector3 position = Vector3Zero(); + Vector3 target = Vector3Zero(); + float distance = CAMERA_DISTANCE; + float fov = CAMERA_FOV; + CameraProjection projection = CAMERA_PERSPECTIVE; + float angle_x = 0.0; + float angle_y = 0.0; -private: - Camera camera; - - Vector3 position; - Vector3 target; - float distance; - float angle_x; - float angle_y; - - // Input - Vector2 last_mouse; - bool rotating; - bool panning; - bool target_lock; + Camera camera = Camera{Vector3(0, 0, -distance), target, Vector3(0, 1.0, 0), + fov, projection}; public: - OrbitCamera3D() - : camera(Camera(Vector3Zero(), Vector3Zero(), Vector3Zero(), 0.0, 0)), - position(Vector3Zero()), target(Vector3Zero()), - distance(CAMERA_DISTANCE), angle_x(0.0), angle_y(0.0), - last_mouse(Vector2Zero()), rotating(false), panning(false), - target_lock(true) { - camera.position = Vector3(0, 0, -1.0 * distance); - camera.target = target; - camera.up = Vector3(0, 1.0, 0); - camera.fovy = CAMERA_FOV; - camera.projection = CAMERA_PERSPECTIVE; - } + auto Rotate(Vector2 last_mouse, Vector2 mouse) -> void; - ~OrbitCamera3D() {} + auto Pan(Vector2 last_mouse, Vector2 mouse) -> void; -public: - auto HandleCameraInput() -> Vector2; - - auto Update(const Vector3 ¤t_target) -> void; + auto Update(const Vector3 ¤t_target, bool lock) -> void; }; #endif diff --git a/include/config.hpp b/include/config.hpp index 56adeb3..a6a9e02 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -10,15 +10,17 @@ // #define TRACY // Enable tracy profiling support // Window -constexpr int INITIAL_WIDTH = 800; -constexpr int INITIAL_HEIGHT = 800; -constexpr int MENU_HEIGHT = 250; +constexpr int INITIAL_WIDTH = 600; +constexpr int INITIAL_HEIGHT = 600; +constexpr int MENU_HEIGHT = 350; // Menu constexpr int MENU_PAD = 5; constexpr int BUTTON_PAD = 12; -constexpr int MENU_ROWS = 5; +constexpr int MENU_ROWS = 7; constexpr int MENU_COLS = 3; +constexpr const char *FONT = "fonts/SpaceMono.ttf"; +constexpr int FONT_SIZE = 26; // Camera Controls constexpr float CAMERA_FOV = 120.0; @@ -26,6 +28,7 @@ constexpr float CAMERA_DISTANCE = 20.0; constexpr float MIN_CAMERA_DISTANCE = 2.0; constexpr float MAX_CAMERA_DISTANCE = 2000.0; constexpr float ZOOM_SPEED = 2.5; +constexpr float FOV_SPEED = 1.0; constexpr float ZOOM_MULTIPLIER = 4.0; constexpr float PAN_SPEED = 2.0; constexpr float PAN_MULTIPLIER = 10.0; @@ -52,13 +55,13 @@ constexpr Color EDGE_COLOR = DARKGREEN; constexpr int DRAW_VERTICES_LIMIT = 1000000; // Klotski Drawing -constexpr int BOARD_PADDING = 5; -constexpr int BLOCK_PADDING = 5; -constexpr Color BLOCK_COLOR = DARKGREEN; -constexpr Color HL_BLOCK_COLOR = GREEN; +constexpr int BOARD_PADDING = 10; +constexpr Color BOARD_COLOR_WON = DARKGREEN; +constexpr Color BOARD_COLOR_RESTRICTED = GRAY; +constexpr Color BOARD_COLOR_FREE = RAYWHITE; +constexpr Color BLOCK_COLOR = DARKBLUE; constexpr Color TARGET_BLOCK_COLOR = RED; -constexpr Color HL_TARGET_BLOCK_COLOR = ORANGE; constexpr Color WALL_COLOR = BLACK; -constexpr Color HL_WALL_COLOR = DARKGRAY; +constexpr Color GOAL_COLOR = ORANGE; #endif diff --git a/include/gui.hpp b/include/gui.hpp new file mode 100644 index 0000000..2990be4 --- /dev/null +++ b/include/gui.hpp @@ -0,0 +1,190 @@ +#ifndef __GUI_HPP_ +#define __GUI_HPP_ + +#include "camera.hpp" +#include "config.hpp" +#include "input.hpp" +#include "state.hpp" + +#include +#include + +class Grid { +public: + int x; + int y; + int width; + int height; + int columns; + int rows; + const int padding; + +public: + Grid(int _x, int _y, int _width, int _height, int _columns, int _rows, + int _padding) + : x(_x), y(_y), width(_width), height(_height), columns(_columns), + rows(_rows), padding(_padding) {} + +public: + auto UpdateBounds(int _x, int _y, int _width, int _height, int _columns, + int _rows) -> void; + + auto UpdateBounds(int _x, int _y, int _width, int _height) -> void; + + auto UpdateBounds(int _x, int _y) -> void; + + auto Bounds() const -> Rectangle; + + auto Bounds(int _x, int _y, int _width, int _height) const -> Rectangle; + + auto SquareBounds() const -> Rectangle; + + auto SquareBounds(int _x, int _y, int _width, int _height) const -> Rectangle; +}; + +class Gui { + struct Style { + int border_color_normal; + int base_color_normal; + int text_color_normal; + + int border_color_focused; + int base_color_focused; + int text_color_focused; + + int border_color_pressed; + int base_color_pressed; + int text_color_pressed; + + int border_color_disabled; + int base_color_disabled; + int text_color_disabled; + }; + + struct DefaultStyle : Style { + int background_color; + int line_color; + + int text_size; + int text_spacing; + int text_line_spacing; + int text_alignment_vertical; + int text_wrap_mode; + }; + + struct ComponentStyle : Style { + int border_width; + int text_padding; + int text_alignment; + }; + +private: + InputHandler &input; + StateManager &state; + const OrbitCamera3D &camera; + + Grid menu_grid = + Grid(0, 0, GetScreenWidth(), MENU_HEIGHT, MENU_COLS, MENU_ROWS, MENU_PAD); + + Grid board_grid = Grid( + 0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, + state.current_state.width, state.current_state.height, BOARD_PADDING); + + Grid graph_overlay_grid = + Grid(GetScreenWidth() / 2, MENU_HEIGHT, 200, 100, 1, 4, MENU_PAD); + + bool save_window = false; + std::array preset_name = {0}; + bool help_window = false; + +public: + Gui(InputHandler &_input, StateManager &_state, const OrbitCamera3D &_camera) + : input(_input), state(_state), camera(_camera) { + Init(); + } + + Gui(const Gui ©) = delete; + Gui &operator=(const Gui ©) = delete; + Gui(Gui &&move) = delete; + Gui &operator=(Gui &&move) = delete; + +private: + auto Init() const -> void; + + auto ApplyColor(Style &style, Color color) const -> void; + + auto ApplyBlockColor(Style &style, Color color) const -> void; + + auto ApplyTextColor(Style &style, Color color) const -> void; + + auto GetDefaultStyle() const -> DefaultStyle; + + auto SetDefaultStyle(const DefaultStyle &style) const -> void; + + auto GetComponentStyle(int component) const -> ComponentStyle; + + auto SetComponentStyle(int component, const ComponentStyle &style) const + -> void; + + auto DrawButton(Rectangle bounds, const std::string &label, Color color, + bool enabled = true, int font_size = FONT_SIZE) const -> int; + + auto DrawMenuButton(int x, int y, int width, int height, + const std::string &label, Color color, + bool enabled = true, int font_size = FONT_SIZE) const + -> int; + + auto DrawToggleSlider(Rectangle bounds, const std::string &off_label, + const std::string &on_label, int *active, Color color, + bool enabled = true, int font_size = FONT_SIZE) const + -> int; + + auto DrawMenuToggleSlider(int x, int y, int width, int height, + const std::string &off_label, + const std::string &on_label, int *active, + Color color, bool enabled = true, + int font_size = FONT_SIZE) const -> int; + + auto DrawSpinner(Rectangle bounds, const std::string &label, int *value, + int min, int max, Color color, bool enabled = true, + int font_size = FONT_SIZE) const -> int; + + auto DrawMenuSpinner(int x, int y, int width, int height, + const std::string &label, int *value, int min, int max, + Color color, bool enabled = true, + int font_size = FONT_SIZE) const -> int; + + auto DrawLabel(Rectangle bounds, const std::string &text, Color color, + bool enabled = true, int font_size = FONT_SIZE) const -> int; + + auto DrawBoardBlock(int x, int y, int width, int height, Color color, + bool enabled = true) const -> bool; + + auto WindowOpen() const -> bool; + + // Different menu sections + auto DrawMenuHeader(Color color) const -> void; + auto DrawGraphInfo(Color color) const -> void; + auto DrawGraphControls(Color color) const -> void; + auto DrawCameraControls(Color color) const -> void; + auto DrawPuzzleControls(Color color) const -> void; + auto DrawEditControls(Color color) const -> void; + auto DrawMenuFooter(Color color) -> void; + +public: + auto GetBackgroundColor() const -> Color; + + auto HelpPopup() -> void; + + auto DrawSavePresetPopup() -> void; + + auto DrawMainMenu() -> void; + + auto DrawPuzzleBoard() -> void; + + auto DrawGraphOverlay(int fps, int ups) -> void; + + auto Update() -> void; +}; + +#endif diff --git a/include/input.hpp b/include/input.hpp index 1832b41..0ec7de5 100644 --- a/include/input.hpp +++ b/include/input.hpp @@ -1,31 +1,71 @@ #ifndef __INPUT_HPP_ #define __INPUT_HPP_ +#include "camera.hpp" #include "config.hpp" #include "state.hpp" +#include +#include +#include + class InputHandler { + struct GenericHandler { + std::function handler; + }; + + struct MouseHandler : GenericHandler { + MouseButton button; + }; + + struct KeyboardHandler : GenericHandler { + KeyboardKey key; + }; + +private: + std::vector generic_handlers; + std::vector mouse_pressed_handlers; + std::vector mouse_released_handlers; + std::vector key_pressed_handlers; + std::vector key_released_handlers; + public: StateManager &state; + OrbitCamera3D &camera; - int hov_x; - int hov_y; - int sel_x; - int sel_y; + bool disable = false; - bool has_block_add_xy; - int block_add_x; - int block_add_y; + // Block selection + int hov_x = -1; + int hov_y = -1; + int sel_x = 0; + int sel_y = 0; - bool mark_path; - bool mark_solutions; - bool connect_solutions; + // Editing + bool editing = false; + bool has_block_add_xy = false; + int block_add_x = -1; + int block_add_y = -1; + + // Graph display + bool mark_path = false; + bool mark_solutions = false; + bool connect_solutions = false; + + // Camera + bool camera_lock = true; + bool camera_panning = false; + bool camera_rotating = false; + + // Mouse dragging + Vector2 mouse = Vector2Zero(); + Vector2 last_mouse = Vector2Zero(); public: - InputHandler(StateManager &_state) - : state(_state), hov_x(-1), hov_y(-1), sel_x(0), sel_y(0), - has_block_add_xy(false), block_add_x(-1), block_add_y(-1), - mark_path(false), mark_solutions(false), connect_solutions(false) {} + InputHandler(StateManager &_state, OrbitCamera3D &_camera) + : state(_state), camera(_camera) { + InitHandlers(); + } InputHandler(const InputHandler ©) = delete; InputHandler &operator=(const InputHandler ©) = delete; @@ -34,12 +74,81 @@ public: ~InputHandler() {} +private: + auto InitHandlers() -> void; + public: - auto HandleMouseHover() -> void; + // Helpers + auto MouseInMenuPane() -> bool; + auto MouseInBoardPane() -> bool; + auto MouseInGraphPane() -> bool; - auto HandleMouse() -> void; + // Mouse actions + auto MouseHover() -> void; + auto CameraStartPan() -> void; + auto CameraPan() -> void; + auto CameraStopPan() -> void; + auto CameraStartRotate() -> void; + auto CameraRotate() -> void; + auto CameraStopRotate() -> void; + auto CameraZoom() -> void; + auto CameraFov() -> void; + auto SelectBlock() -> void; + auto StartAddBlock() -> void; + auto ClearAddBlock() -> void; + auto AddBlock() -> void; + auto RemoveBlock() -> void; + auto PlaceGoal() -> void; - auto HandleKeys() -> void; + // Key actions + auto ToggleCameraLock() -> void; + auto ToggleCameraProjection() -> void; + auto MoveBlockNor() -> void; + auto MoveBlockWes() -> void; + auto MoveBlockSou() -> void; + auto MoveBlockEas() -> void; + auto PrintState() const -> void; + auto PreviousPreset() -> void; + auto NextPreset() -> void; + auto ResetState() -> void; + auto FillGraph() -> void; + auto ClearGraph() -> void; + auto ToggleMarkSolutions() -> void; + auto ToggleConnectSolutions() -> void; + auto ToggleMarkPath() -> void; + auto MakeOptimalMove() -> void; + auto GoToWorstState() -> void; + auto GoToNearestTarget() -> void; + auto UndoLastMove() -> void; + auto ToggleRestrictedMovement() -> void; + auto ToggleTargetBlock() -> void; + auto ToggleWallBlock() -> void; + auto RemoveBoardColumn() -> void; + auto AddBoardColumn() -> void; + auto RemoveBoardRow() -> void; + auto AddBoardRow() -> void; + auto ToggleEditing() -> void; + auto ClearGoal() -> void; + + // General + auto RegisterGenericHandler(std::function handler) + -> void; + + auto RegisterMousePressedHandler(MouseButton button, + std::function handler) + -> void; + + auto RegisterMouseReleasedHandler(MouseButton button, + std::function handler) + -> void; + + auto RegisterKeyPressedHandler(KeyboardKey key, + std::function handler) + -> void; + + auto RegisterKeyReleasedHandler(KeyboardKey key, + std::function handler) + -> void; auto HandleInput() -> void; }; diff --git a/include/octree.hpp b/include/octree.hpp index 9793d56..4cec38b 100644 --- a/include/octree.hpp +++ b/include/octree.hpp @@ -49,8 +49,6 @@ public: -> void; auto CalculateForce(int node_idx, const Vector3 &pos) const -> Vector3; - - auto Print() const -> void; }; #endif diff --git a/include/physics.hpp b/include/physics.hpp index e969663..0c3209f 100644 --- a/include/physics.hpp +++ b/include/physics.hpp @@ -7,8 +7,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -78,12 +78,10 @@ public: : threads(std::thread::hardware_concurrency() - 1, SetThreadName) #endif { - std::cout << "Using Barnes-Hut + octree repulsion force calculation." - << std::endl; + std::println("Using Barnes-Hut + Octree repulsion force calculation."); #ifdef THREADPOOL - std::cout << "Thread-Pool: " << threads.get_thread_count() << " threads." - << std::endl; + std::println("Thread-pool: {} threads.", threads.get_thread_count()); #endif }; diff --git a/include/puzzle.hpp b/include/puzzle.hpp index c646e8a..fa0912c 100644 --- a/include/puzzle.hpp +++ b/include/puzzle.hpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include #include @@ -40,7 +40,7 @@ public: : x(_x), y(_y), width(_width), height(_height), target(_target), immovable(_immovable) { if (_x < 0 || _x + _width >= 10 || _y < 0 || _y + _height >= 10) { - std::cerr << "Block must fit on a 9x9 board!" << std::endl; + std::println("Block must fit in a 9x9 board!"); exit(1); } } @@ -92,11 +92,11 @@ public: } if (_x < 0 || _x + width >= 10 || _y < 0 || _y + height >= 10) { - std::cerr << "Block must fit on a 9x9 board!" << std::endl; + std::println("Block must fit in a 9x9 board!"); exit(1); } if (block.length() != 2) { - std::cerr << "Block representation must have length [2]!" << std::endl; + std::println("Block representation must have length 2!"); exit(1); } } @@ -109,7 +109,7 @@ public: bool operator!=(const Block &other) { return !(*this == other); } public: - auto Hash() const -> int; + auto Hash() const -> std::size_t; static auto Invalid() -> Block; @@ -186,13 +186,12 @@ public: _height, _target_x, _target_y, std::string(_width * _height * 2, '.'))) { if (_width < 1 || _width > 9 || _height < 1 || _height > 9) { - std::cerr << "State width/height must be in [1, 9]!" << std::endl; + std::println("State width/height must be in [1, 9]!"); exit(1); } if (_target_x < 0 || _target_x >= 9 || _target_y < 0 || _target_y >= 9) { if (_target_x != 9 && _target_y != 9) { - std::cerr << "State target must be within the board bounds!" - << std::endl; + std::println("State target must be within the board bounds!"); exit(1); } } @@ -210,20 +209,19 @@ public: target_y(std::stoi(_state.substr(4, 1))), restricted(_state.substr(0, 1) == "R"), state(_state) { if (width < 1 || width > 9 || height < 1 || height > 9) { - std::cerr << "State width/height must be in [1, 9]!" << std::endl; + std::println("State width/height must be in [1, 9]!"); exit(1); } if (target_x < 0 || target_x >= 9 || target_y < 0 || target_y >= 9) { if (target_x != 9 && target_y != 9) { - std::cerr << "State target must be within the board bounds!" - << std::endl; + std::println("State target must be within the board bounds!"); exit(1); } } if (static_cast(_state.length()) != width * height * 2 + prefix) { - std::cerr - << "State representation must have length [width * height * 2 + " - << prefix << "]!" << std::endl; + std::println( + "State representation must have length width * height * 2 + {}!", + prefix); exit(1); } } @@ -243,7 +241,7 @@ public: BlockIterator end() const { return BlockIterator(*this, width * height); } public: - auto Hash() const -> int; + auto Hash() const -> std::size_t; auto HasWinCondition() const -> bool; @@ -251,6 +249,8 @@ public: auto SetGoal(int x, int y) -> bool; + auto ClearGoal() -> void; + auto AddColumn() const -> State; auto RemoveColumn() const -> State; diff --git a/include/renderer.hpp b/include/renderer.hpp index fecf104..8d48139 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -3,6 +3,7 @@ #include "camera.hpp" #include "config.hpp" +#include "gui.hpp" #include "input.hpp" #include "state.hpp" @@ -13,6 +14,7 @@ class Renderer { private: const StateManager &state; const InputHandler &input; + Gui &gui; const OrbitCamera3D &camera; RenderTexture render_target; @@ -21,16 +23,15 @@ private: // Instancing Material vertex_mat; - std::size_t transforms_size; - Matrix *transforms; + std::size_t transforms_size = 0; + Matrix *transforms = nullptr; Mesh cube_instance; Shader instancing_shader; public: Renderer(const OrbitCamera3D &_camera, const StateManager &_state, - const InputHandler &_input) - : state(_state), input(_input), camera(_camera), transforms_size(0), - transforms(nullptr) { + const InputHandler &_input, Gui &_gui) + : state(_state), input(_input), gui(_gui), camera(_camera) { render_target = LoadRenderTexture(GetScreenWidth() / 2.0, GetScreenHeight() - MENU_HEIGHT); klotski_target = LoadRenderTexture(GetScreenWidth() / 2.0, @@ -73,7 +74,7 @@ public: auto DrawMenu(const std::vector &masses) -> void; - auto DrawTextures(float ups) -> void; + auto DrawTextures(int fps, int ups) -> void; }; #endif diff --git a/include/state.hpp b/include/state.hpp index 392cb04..37a2a36 100644 --- a/include/state.hpp +++ b/include/state.hpp @@ -15,17 +15,17 @@ class StateManager { public: ThreadedPhysics &physics; - std::vector presets; - std::vector comments; + std::vector presets = {State()}; + std::vector comments = {"Empty"}; // Some stuff is faster to map from state to mass (e.g. in the renderer) - std::unordered_map states; + std::unordered_map states; // State to mass id std::unordered_set winning_states; - std::unordered_set visited_states; + std::unordered_map visited_states; // How often we've been here std::stack history; // Other stuff maps from mass to state :/ - std::unordered_map masses; + std::unordered_map masses; // Mass id to state std::vector winning_path; // Fuck it, duplicate the springs too, we don't even need to copy them from @@ -36,7 +36,10 @@ public: // path on the same graph DistanceResult target_distances; - int current_preset; + std::string preset_file; + + int total_moves = 0; + int current_preset = 0; State starting_state; State current_state; State previous_state; @@ -45,8 +48,7 @@ public: public: StateManager(ThreadedPhysics &_physics, const std::string &preset_file) - : physics(_physics), presets({State()}), current_preset(0), - edited(false) { + : physics(_physics) { ParsePresetFile(preset_file); current_state = presets.at(current_preset); ClearGraph(); @@ -60,9 +62,11 @@ public: ~StateManager() {} private: - auto ParsePresetFile(const std::string &preset_file) -> void; + auto ParsePresetFile(const std::string &_preset_file) -> bool; public: + auto AppendPresetFile(const std::string preset_name) -> void; + auto LoadPreset(int preset) -> void; auto ResetState() -> void; diff --git a/src/camera.cpp b/src/camera.cpp index 97b076c..1cca5d6 100644 --- a/src/camera.cpp +++ b/src/camera.cpp @@ -9,78 +9,40 @@ #include #endif -auto OrbitCamera3D::HandleCameraInput() -> Vector2 { - Vector2 mouse = GetMousePosition(); - if (mouse.x >= GetScreenWidth() / 2.0 && mouse.y >= MENU_HEIGHT) { - if (IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) { - rotating = true; - last_mouse = mouse; - } else if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { - panning = true; - target_lock = false; - last_mouse = mouse; - } +auto OrbitCamera3D::Rotate(Vector2 last_mouse, Vector2 mouse) -> void { + Vector2 dx = Vector2Subtract(mouse, last_mouse); - // Zoom - float wheel = GetMouseWheelMove(); - if (IsKeyDown(KEY_LEFT_SHIFT)) { - distance -= wheel * ZOOM_SPEED * ZOOM_MULTIPLIER; - } else { - distance -= wheel * ZOOM_SPEED; - } - } + angle_x -= dx.x * ROT_SPEED / 200.0; + angle_y += dx.y * ROT_SPEED / 200.0; - if (IsMouseButtonReleased(MOUSE_RIGHT_BUTTON)) { - rotating = false; - } - if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { - panning = false; - } - - if (IsKeyPressed(KEY_L)) { - target_lock = !target_lock; - } - return mouse; + angle_y = Clamp(angle_y, -1.5, 1.5); // Prevent flipping } -auto OrbitCamera3D::Update(const Vector3 ¤t_target) -> void { - Vector2 mouse = HandleCameraInput(); +auto OrbitCamera3D::Pan(Vector2 last_mouse, Vector2 mouse) -> void { + Vector2 dx = Vector2Subtract(mouse, last_mouse); - if (rotating) { - Vector2 dx = Vector2Subtract(mouse, last_mouse); - last_mouse = mouse; - - angle_x -= dx.x * ROT_SPEED / 200.0; - angle_y += dx.y * ROT_SPEED / 200.0; - - angle_y = Clamp(angle_y, -1.5, 1.5); // Prevent flipping + float speed; + if (IsKeyDown(KEY_LEFT_SHIFT)) { + speed = distance * PAN_SPEED / 1000.0 * PAN_MULTIPLIER; + } else { + speed = distance * PAN_SPEED / 1000.0; } - if (panning) { - Vector2 dx = Vector2Subtract(mouse, last_mouse); - last_mouse = mouse; + // The panning needs to happen in camera coordinates, otherwise rotating the + // camera breaks it + Vector3 forward = + Vector3Normalize(Vector3Subtract(camera.target, camera.position)); + Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up)); + Vector3 up = Vector3Normalize(Vector3CrossProduct(right, forward)); - float speed; - if (IsKeyDown(KEY_LEFT_SHIFT)) { - speed = distance * PAN_SPEED / 1000.0 * PAN_MULTIPLIER; - } else { - speed = distance * PAN_SPEED / 1000.0; - } + Vector3 offset = Vector3Add(Vector3Scale(right, -dx.x * speed), + Vector3Scale(up, dx.y * speed)); - // The panning needs to happen in camera coordinates, otherwise rotating the - // camera breaks it - Vector3 forward = - Vector3Normalize(Vector3Subtract(camera.target, camera.position)); - Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up)); - Vector3 up = Vector3Normalize(Vector3CrossProduct(right, forward)); + target = Vector3Add(target, offset); +} - Vector3 offset = Vector3Add(Vector3Scale(right, -dx.x * speed), - Vector3Scale(up, dx.y * speed)); - - target = Vector3Add(target, offset); - } - - if (target_lock) { +auto OrbitCamera3D::Update(const Vector3 ¤t_target, bool lock) -> void { + if (lock) { target = Vector3MoveTowards( target, current_target, CAMERA_SMOOTH_SPEED * GetFrameTime() * @@ -88,12 +50,20 @@ auto OrbitCamera3D::Update(const Vector3 ¤t_target) -> void { } distance = Clamp(distance, MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE); + int actual_distance = distance; + if (projection == CAMERA_ORTHOGRAPHIC) { + actual_distance = MAX_CAMERA_DISTANCE; + } // Spherical coordinates - float x = cos(angle_y) * sin(angle_x) * distance; - float y = sin(angle_y) * distance; - float z = cos(angle_y) * cos(angle_x) * distance; + float x = cos(angle_y) * sin(angle_x) * actual_distance; + float y = sin(angle_y) * actual_distance; + float z = cos(angle_y) * cos(angle_x) * actual_distance; + + fov = Clamp(fov, 25.0, 155.0); camera.position = Vector3Add(target, Vector3(x, y, z)); camera.target = target; + camera.fovy = fov; + camera.projection = projection; } diff --git a/src/distance.cpp b/src/distance.cpp index da7cdc3..82bd7fe 100644 --- a/src/distance.cpp +++ b/src/distance.cpp @@ -30,14 +30,6 @@ auto CalculateDistances( adjacency[from].push_back(to); adjacency[to].push_back(from); } - // for (size_t i = 0; i < adjacency.size(); ++i) { - // std::cout << "Node " << i << "'s neighbors: "; - // for (const auto &neighbor : adjacency[i]) { - // std::cout << neighbor; - // } - // std::cout << "\n"; - // } - // std::cout << std::endl; std::vector distances(node_count, -1); std::vector parents(node_count, -1); diff --git a/src/gui.cpp b/src/gui.cpp new file mode 100644 index 0000000..57af3a5 --- /dev/null +++ b/src/gui.cpp @@ -0,0 +1,792 @@ +#include "gui.hpp" +#include "config.hpp" + +#include + +#define RAYGUI_IMPLEMENTATION +#include + +#ifdef TRACY +#include "tracy.hpp" +#include +#endif + +auto Grid::UpdateBounds(int _x, int _y, int _width, int _height, int _columns, + int _rows) -> void { + x = _x; + y = _y; + width = _width; + height = _height; + columns = _columns; + rows = _rows; +} + +auto Grid::UpdateBounds(int _x, int _y, int _width, int _height) -> void { + x = _x; + y = _y; + width = _width; + height = _height; +} + +auto Grid::UpdateBounds(int _x, int _y) -> void { + x = _x; + y = _y; +} + +auto Grid::Bounds() const -> Rectangle { + Rectangle bounds = Bounds(0, 0, columns, rows); + bounds.x -= padding; + bounds.y -= padding; + bounds.width += 2 * padding; + bounds.height += 2 * padding; + return bounds; +} + +auto Grid::Bounds(int _x, int _y, int _width, int _height) const -> Rectangle { + if (_x < 0 || _x + _width > columns || _y < 0 || _y + _height > rows) { + std::println("Grid bounds are outside range."); + exit(1); + } + + int cell_width = (width - padding) / columns; + int cell_height = (height - padding) / rows; + + return Rectangle( + x + _x * cell_width + padding, y + _y * cell_height + padding, + _width * cell_width - padding, _height * cell_height - padding); +} + +auto Grid::SquareBounds() const -> Rectangle { + Rectangle bounds = SquareBounds(0, 0, columns, rows); + bounds.x -= padding; + bounds.y -= padding; + bounds.width += 2 * padding; + bounds.height += 2 * padding; + return bounds; +} + +auto Grid::SquareBounds(int _x, int _y, int _width, int _height) const + -> Rectangle { + // Assumes each cell is square, so either width or height are not completely + // filled + + if (_x < 0 || _x + _width > columns || _y < 0 || _y + _height > rows) { + std::println("Grid bounds are outside range."); + exit(1); + } + + int available_width = width - padding * (columns + 1); + int available_height = height - padding * (rows + 1); + int cell_size = std::min(available_width / columns, available_height / rows); + + int grid_width = cell_size * columns + padding * (columns + 1); + int grid_height = cell_size * rows + padding * (rows + 1); + int x_offset = (width - grid_width) / 2; + int y_offset = (height - grid_height) / 2; + + return Rectangle(x_offset + _x * (cell_size + padding) + padding, + y_offset + _y * (cell_size + padding) + padding, + _width * cell_size + padding * (_width - 1), + _height * cell_size + padding * (_height - 1)); +} + +auto Gui::Init() const -> void { + Font font = LoadFontEx(FONT, FONT_SIZE, 0, 0); + SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR); + GuiSetFont(font); + + DefaultStyle style = GetDefaultStyle(); + style.text_size = FONT_SIZE; + ApplyColor(style, GRAY); + + SetDefaultStyle(style); +} + +auto Gui::ApplyColor(Style &style, Color color) const -> void { + style.base_color_normal = ColorToInt(Fade(color, 0.5)); + style.base_color_focused = ColorToInt(Fade(color, 0.3)); + style.base_color_pressed = ColorToInt(Fade(color, 0.8)); + style.base_color_disabled = style.base_color_normal; + + style.border_color_normal = ColorToInt(Fade(color, 1.0)); + style.border_color_focused = ColorToInt(Fade(color, 0.7)); + style.border_color_pressed = ColorToInt(Fade(color, 1.0)); + style.border_color_disabled = style.base_color_normal; + + style.text_color_normal = ColorToInt(Fade(BLACK, 0.7)); + style.text_color_focused = ColorToInt(Fade(BLACK, 0.7)); + style.text_color_pressed = ColorToInt(Fade(BLACK, 0.7)); + style.text_color_disabled = style.text_color_normal; +} + +auto Gui::ApplyBlockColor(Style &style, Color color) const -> void { + style.base_color_normal = ColorToInt(Fade(color, 0.5)); + style.base_color_focused = ColorToInt(Fade(color, 0.3)); + style.base_color_pressed = ColorToInt(Fade(color, 0.8)); + style.base_color_disabled = style.base_color_normal; + + style.border_color_normal = ColorToInt(Fade(color, 1.0)); + style.border_color_focused = ColorToInt(Fade(color, 0.7)); + style.border_color_pressed = ColorToInt(Fade(color, 1.0)); + style.border_color_disabled = style.base_color_normal; +} + +auto Gui::ApplyTextColor(Style &style, Color color) const -> void { + style.text_color_normal = ColorToInt(Fade(color, 1.0)); + style.text_color_focused = ColorToInt(Fade(color, 1.0)); + style.text_color_pressed = ColorToInt(Fade(color, 1.0)); + style.text_color_disabled = ColorToInt(Fade(BLACK, 0.7)); +} + +auto Gui::GetDefaultStyle() const -> DefaultStyle { + // Could've iterated over the values but then it wouldn't be as nice to + // access... + return {GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL), + GuiGetStyle(DEFAULT, BASE_COLOR_NORMAL), + GuiGetStyle(DEFAULT, TEXT_COLOR_NORMAL), + + GuiGetStyle(DEFAULT, BORDER_COLOR_FOCUSED), + GuiGetStyle(DEFAULT, BASE_COLOR_FOCUSED), + GuiGetStyle(DEFAULT, TEXT_COLOR_FOCUSED), + + GuiGetStyle(DEFAULT, BORDER_COLOR_PRESSED), + GuiGetStyle(DEFAULT, BASE_COLOR_PRESSED), + GuiGetStyle(DEFAULT, TEXT_COLOR_PRESSED), + + GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED), + GuiGetStyle(DEFAULT, BASE_COLOR_DISABLED), + GuiGetStyle(DEFAULT, TEXT_COLOR_DISABLED), + + GuiGetStyle(DEFAULT, BACKGROUND_COLOR), + GuiGetStyle(DEFAULT, LINE_COLOR), + + GuiGetStyle(DEFAULT, TEXT_SIZE), + GuiGetStyle(DEFAULT, TEXT_SPACING), + GuiGetStyle(DEFAULT, TEXT_LINE_SPACING), + GuiGetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL), + GuiGetStyle(DEFAULT, TEXT_WRAP_MODE)}; +} + +auto Gui::SetDefaultStyle(const DefaultStyle &style) const -> void { + GuiSetStyle(DEFAULT, BORDER_COLOR_NORMAL, style.border_color_normal); + GuiSetStyle(DEFAULT, BASE_COLOR_NORMAL, style.base_color_normal); + GuiSetStyle(DEFAULT, TEXT_COLOR_NORMAL, style.text_color_normal); + + GuiSetStyle(DEFAULT, BORDER_COLOR_FOCUSED, style.border_color_focused); + GuiSetStyle(DEFAULT, BASE_COLOR_FOCUSED, style.base_color_focused); + GuiSetStyle(DEFAULT, TEXT_COLOR_FOCUSED, style.text_color_focused); + + GuiSetStyle(DEFAULT, BORDER_COLOR_PRESSED, style.border_color_pressed); + GuiSetStyle(DEFAULT, BASE_COLOR_PRESSED, style.base_color_pressed); + GuiSetStyle(DEFAULT, TEXT_COLOR_PRESSED, style.text_color_pressed); + + GuiSetStyle(DEFAULT, BORDER_COLOR_DISABLED, style.border_color_disabled); + GuiSetStyle(DEFAULT, BASE_COLOR_DISABLED, style.base_color_disabled); + GuiSetStyle(DEFAULT, TEXT_COLOR_DISABLED, style.text_color_disabled); + + GuiSetStyle(DEFAULT, BACKGROUND_COLOR, style.background_color); + GuiSetStyle(DEFAULT, LINE_COLOR, style.line_color); + + GuiSetStyle(DEFAULT, TEXT_SIZE, style.text_size); + GuiSetStyle(DEFAULT, TEXT_SPACING, style.text_spacing); + GuiSetStyle(DEFAULT, TEXT_LINE_SPACING, style.text_line_spacing); + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL, style.text_alignment_vertical); + GuiSetStyle(DEFAULT, TEXT_WRAP_MODE, style.text_wrap_mode); +} + +auto Gui::GetComponentStyle(int component) const -> ComponentStyle { + return {GuiGetStyle(component, BORDER_COLOR_NORMAL), + GuiGetStyle(component, BASE_COLOR_NORMAL), + GuiGetStyle(component, TEXT_COLOR_NORMAL), + + GuiGetStyle(component, BORDER_COLOR_FOCUSED), + GuiGetStyle(component, BASE_COLOR_FOCUSED), + GuiGetStyle(component, TEXT_COLOR_FOCUSED), + + GuiGetStyle(component, BORDER_COLOR_PRESSED), + GuiGetStyle(component, BASE_COLOR_PRESSED), + GuiGetStyle(component, TEXT_COLOR_PRESSED), + + GuiGetStyle(component, BORDER_COLOR_DISABLED), + GuiGetStyle(component, BASE_COLOR_DISABLED), + GuiGetStyle(component, TEXT_COLOR_DISABLED), + + GuiGetStyle(component, BORDER_WIDTH), + GuiGetStyle(component, TEXT_PADDING), + GuiGetStyle(component, TEXT_ALIGNMENT)}; +} + +auto Gui::SetComponentStyle(int component, const ComponentStyle &style) const + -> void { + GuiSetStyle(component, BORDER_COLOR_NORMAL, style.border_color_normal); + GuiSetStyle(component, BASE_COLOR_NORMAL, style.base_color_normal); + GuiSetStyle(component, TEXT_COLOR_NORMAL, style.text_color_normal); + + GuiSetStyle(component, BORDER_COLOR_FOCUSED, style.border_color_focused); + GuiSetStyle(component, BASE_COLOR_FOCUSED, style.base_color_focused); + GuiSetStyle(component, TEXT_COLOR_FOCUSED, style.text_color_focused); + + GuiSetStyle(component, BORDER_COLOR_PRESSED, style.border_color_pressed); + GuiSetStyle(component, BASE_COLOR_PRESSED, style.base_color_pressed); + GuiSetStyle(component, TEXT_COLOR_PRESSED, style.text_color_pressed); + + GuiSetStyle(component, BORDER_COLOR_DISABLED, style.border_color_disabled); + GuiSetStyle(component, BASE_COLOR_DISABLED, style.base_color_disabled); + GuiSetStyle(component, TEXT_COLOR_DISABLED, style.text_color_disabled); + + GuiSetStyle(component, BORDER_WIDTH, style.border_width); + GuiSetStyle(component, TEXT_PADDING, style.text_padding); + GuiSetStyle(component, TEXT_ALIGNMENT, style.text_alignment); +} + +auto Gui::DrawButton(Rectangle bounds, const std::string &label, Color color, + bool enabled, int font_size) const -> int { + // Save original styling + const DefaultStyle original_default = GetDefaultStyle(); + const ComponentStyle original_button = GetComponentStyle(BUTTON); + + // Change styling + DefaultStyle style_default = original_default; + ComponentStyle style_button = original_button; + style_default.text_size = font_size; + ApplyColor(style_button, color); + SetDefaultStyle(style_default); + SetComponentStyle(BUTTON, style_button); + + const int state = GuiGetState(); + if (!enabled || WindowOpen()) { + GuiSetState(STATE_DISABLED); + } + int pressed = GuiButton(bounds, label.data()); + if (!enabled || WindowOpen()) { + GuiSetState(state); + } + + // Restore original styling + SetDefaultStyle(original_default); + SetComponentStyle(BUTTON, original_button); + + return pressed; +} + +auto Gui::DrawMenuButton(int x, int y, int width, int height, + const std::string &label, Color color, bool enabled, + int font_size) const -> int { + Rectangle bounds = menu_grid.Bounds(x, y, width, height); + return DrawButton(bounds, label, color, enabled, font_size); +} + +auto Gui::DrawToggleSlider(Rectangle bounds, const std::string &off_label, + const std::string &on_label, int *active, + Color color, bool enabled, int font_size) const + -> int { + // Save original styling + const DefaultStyle original_default = GetDefaultStyle(); + const ComponentStyle original_slider = GetComponentStyle(SLIDER); + const ComponentStyle original_toggle = GetComponentStyle(TOGGLE); + + // Change styling + DefaultStyle style_default = original_default; + ComponentStyle style_slider = original_slider; + ComponentStyle style_toggle = original_toggle; + style_default.text_size = font_size; + ApplyColor(style_slider, color); + ApplyColor(style_toggle, color); + SetDefaultStyle(style_default); + SetComponentStyle(SLIDER, style_slider); + SetComponentStyle(TOGGLE, style_toggle); + + const int state = GuiGetState(); + if (!enabled || WindowOpen()) { + GuiSetState(STATE_DISABLED); + } + int pressed = GuiToggleSlider( + bounds, std::format("{};{}", off_label, on_label).data(), active); + if (!enabled || WindowOpen()) { + GuiSetState(state); + } + + // Restore original styling + SetDefaultStyle(original_default); + SetComponentStyle(SLIDER, original_slider); + SetComponentStyle(TOGGLE, original_toggle); + + return pressed; +} + +auto Gui::DrawMenuToggleSlider(int x, int y, int width, int height, + const std::string &off_label, + const std::string &on_label, int *active, + Color color, bool enabled, int font_size) const + -> int { + Rectangle bounds = menu_grid.Bounds(x, y, width, height); + return DrawToggleSlider(bounds, off_label, on_label, active, color, enabled, + font_size); +}; + +auto Gui::DrawSpinner(Rectangle bounds, const std::string &label, int *value, + int min, int max, Color color, bool enabled, + int font_size) const -> int { + // Save original styling + const DefaultStyle original_default = GetDefaultStyle(); + const ComponentStyle original_valuebox = GetComponentStyle(VALUEBOX); + const ComponentStyle original_button = GetComponentStyle(BUTTON); + + // Change styling + DefaultStyle style_default = original_default; + ComponentStyle style_valuebox = original_valuebox; + ComponentStyle style_button = original_button; + style_default.text_size = font_size; + ApplyColor(style_valuebox, color); + ApplyColor(style_button, color); + SetDefaultStyle(style_default); + SetComponentStyle(VALUEBOX, style_valuebox); + SetComponentStyle(BUTTON, style_button); + + const int state = GuiGetState(); + if (!enabled || WindowOpen()) { + GuiSetState(STATE_DISABLED); + } + int pressed = GuiSpinner(bounds, "", label.data(), value, min, max, false); + if (!enabled || WindowOpen()) { + GuiSetState(state); + } + + // Restore original styling + SetDefaultStyle(original_default); + SetComponentStyle(VALUEBOX, original_valuebox); + SetComponentStyle(BUTTON, style_button); + + return pressed; +} + +auto Gui::DrawMenuSpinner(int x, int y, int width, int height, + const std::string &label, int *value, int min, + int max, Color color, bool enabled, + int font_size) const -> int { + Rectangle bounds = menu_grid.Bounds(x, y, width, height); + return DrawSpinner(bounds, label, value, min, max, color, enabled, font_size); +} + +auto Gui::DrawLabel(Rectangle bounds, const std::string &text, Color color, + bool enabled, int font_size) const -> int { + // Save original styling + const DefaultStyle original_default = GetDefaultStyle(); + const ComponentStyle original_label = GetComponentStyle(LABEL); + + // Change styling + DefaultStyle style_default = original_default; + ComponentStyle style_label = original_label; + style_default.text_size = font_size; + ApplyTextColor(style_label, color); + SetDefaultStyle(style_default); + SetComponentStyle(LABEL, style_label); + + const int state = GuiGetState(); + if (!enabled || WindowOpen()) { + GuiSetState(STATE_DISABLED); + } + int pressed = GuiLabel(bounds, text.data()); + if (!enabled || WindowOpen()) { + GuiSetState(state); + } + + // Restore original styling + SetDefaultStyle(original_default); + SetComponentStyle(LABEL, original_label); + + return pressed; +} + +auto Gui::DrawBoardBlock(int x, int y, int width, int height, Color color, + bool enabled) const -> bool { + ComponentStyle style = GetComponentStyle(BUTTON); + ApplyBlockColor(style, color); + + Rectangle bounds = board_grid.SquareBounds(x, y, width, height); + + bool focused = + CheckCollisionPointRec(input.mouse - Vector2(0, MENU_HEIGHT), bounds); + bool pressed = + Block(x, y, width, height, false).Covers(input.sel_x, input.sel_y); + + // Background to make faded colors work + DrawRectangleRec(bounds, RAYWHITE); + + Color base = GetColor(style.base_color_normal); + Color border = GetColor(style.base_color_normal); + if (pressed) { + base = GetColor(style.base_color_pressed); + border = GetColor(style.base_color_pressed); + } + if (focused) { + base = GetColor(style.base_color_focused); + border = GetColor(style.base_color_focused); + } + if (focused && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { + base = GetColor(style.base_color_pressed); + border = GetColor(style.base_color_pressed); + } + if (!enabled) { + base = BOARD_COLOR_RESTRICTED; + } + DrawRectangleRec(bounds, base); + if (enabled) { + DrawRectangleLinesEx(bounds, 2.0, border); + } + + return focused && enabled; +} + +auto Gui::WindowOpen() const -> bool { return save_window || help_window; } + +auto Gui::DrawMenuHeader(Color color) const -> void { + int preset = state.current_preset; + DrawMenuSpinner(0, 0, 1, 1, "Preset: ", &preset, -1, state.presets.size(), + color, !input.editing); + if (preset > state.current_preset) { + input.NextPreset(); + } else if (preset < state.current_preset) { + input.PreviousPreset(); + } + + DrawMenuButton(1, 0, 1, 1, + std::format("Puzzle: \"{}\"", + state.comments.at(state.current_preset).substr(2)), + color, false); + + int editing = input.editing; + DrawMenuToggleSlider(2, 0, 1, 1, "Puzzle Mode (Tab)", "Edit Mode (Tab)", + &editing, color); + if (editing != input.editing) { + input.ToggleEditing(); + } +} + +auto Gui::DrawGraphInfo(Color color) const -> void { + DrawMenuButton(0, 1, 1, 1, + std::format("Found {} States ({} Winning)", + state.states.size(), state.winning_states.size()), + color, false); + + DrawMenuButton(1, 1, 1, 1, + std::format("Found {} Transitions", state.springs.size()), + color, false); + + DrawMenuButton(2, 1, 1, 1, + std::format("{} Moves to Nearest Solution", + state.winning_path.size() > 0 + ? state.winning_path.size() - 1 + : 0), + color, false); +} + +auto Gui::DrawGraphControls(Color color) const -> void { + if (DrawMenuButton(1, 2, 1, 1, "Populate Graph (G)", color)) { + input.FillGraph(); + } + + int mark_path = input.mark_path; + DrawMenuToggleSlider(2, 2, 1, 1, "Path Hidden (U)", "Path Shown (U)", + &mark_path, color); + if (mark_path != input.mark_path) { + input.ToggleMarkPath(); + } + + if (DrawMenuButton(1, 3, 1, 1, "Clear Graph (C)", color)) { + input.ClearGraph(); + } + + int mark_solutions = input.mark_solutions; + DrawMenuToggleSlider(2, 3, 1, 1, "Solutions Hidden (I)", + "Solutions Shown (I)", &mark_solutions, color); + if (mark_solutions != input.mark_solutions) { + input.ToggleMarkSolutions(); + } +} + +auto Gui::DrawCameraControls(Color color) const -> void { + int lock_camera = input.camera_lock; + DrawMenuToggleSlider(0, 2, 1, 1, "Free Camera (L)", "Locked Camera (L)", + &lock_camera, color); + if (lock_camera != input.camera_lock) { + input.ToggleCameraLock(); + } + + int projection = camera.projection == CAMERA_ORTHOGRAPHIC; + DrawMenuToggleSlider(0, 3, 1, 1, "Perspective (Alt)", "Orthographic (Alt)", + &projection, color); + if (projection != camera.projection == CAMERA_ORTHOGRAPHIC) { + input.ToggleCameraProjection(); + } +} + +auto Gui::DrawPuzzleControls(Color color) const -> void { + auto nth = [&](int n) { + if (n == 11 || n == 12 || n == 13) + return "th"; + if (n % 10 == 1) + return "st"; + if (n % 10 == 2) + return "nd"; + if (n % 10 == 3) + return "rd"; + return "th"; + }; + int visits = state.visited_states.at(state.current_state); + DrawMenuButton(0, 4, 1, 1, + std::format("{} Moves ({}{} Time at this State)", + state.total_moves, visits, nth(visits)), + color, false); + + if (DrawMenuButton(1, 4, 1, 1, "Make Optimal Move (Space)", color)) { + input.MakeOptimalMove(); + } + + if (DrawMenuButton(2, 4, 1, 1, "Undo Last Move (Backspace)", color)) { + input.UndoLastMove(); + } + + if (DrawMenuButton(0, 5, 1, 1, "Go to Nearest Solution (B)", color)) { + input.GoToNearestTarget(); + } + + if (DrawMenuButton(1, 5, 1, 1, "Go to Worst State (V)", color)) { + input.GoToWorstState(); + } + + if (DrawMenuButton(2, 5, 1, 1, "Go to Starting State (R)", color)) { + input.ResetState(); + } +} + +auto Gui::DrawEditControls(Color color) const -> void { + // Toggle Target Block + if (DrawMenuButton(0, 4, 1, 1, "Toggle Target Block (T)", color)) { + input.ToggleTargetBlock(); + } + + // Toggle Wall Block + if (DrawMenuButton(0, 5, 1, 1, "Toggle Wall Block (Y)", color)) { + input.ToggleWallBlock(); + } + + // Toggle Restricted/Free Block Movement + int free = !state.current_state.restricted; + DrawMenuToggleSlider(1, 4, 1, 1, "Restricted (F)", "Free (F)", &free, color); + if (free != !state.current_state.restricted) { + input.ToggleRestrictedMovement(); + } + + // Clear Goal + if (DrawMenuButton(1, 5, 1, 1, "Clear Goal (X)", color)) { + } + + // Column Count Spinner + int columns = state.current_state.width; + DrawMenuSpinner(2, 4, 1, 1, "Cols: ", &columns, 1, 9, color); + if (columns > state.current_state.width) { + input.AddBoardColumn(); + } else if (columns < state.current_state.width) { + input.RemoveBoardColumn(); + } + + // Row Count Spinner + int rows = state.current_state.height; + DrawMenuSpinner(2, 5, 1, 1, "Rows: ", &rows, 1, 9, color); + if (rows > state.current_state.height) { + input.AddBoardRow(); + } else if (rows < state.current_state.height) { + input.RemoveBoardRow(); + } +} + +auto Gui::DrawMenuFooter(Color color) -> void { + DrawMenuButton(0, 6, 2, 1, + std::format("State: \"{}\"", state.current_state.state), color, + false); + + if (DrawMenuButton(2, 6, 1, 1, "Save as Preset", color)) { + save_window = true; + } +} + +auto Gui::GetBackgroundColor() const -> Color { + return GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)); +} + +auto Gui::HelpPopup() -> void {} + +auto Gui::DrawSavePresetPopup() -> void { + if (!save_window) { + return; + } + + int width = 450; + int height = 150; + // Returns the pressed button index + int button = GuiTextInputBox(Rectangle((GetScreenWidth() - width) / 2.0, + (GetScreenHeight() - height) / 2.0, + width, height), + "Save as Preset", "Enter Preset Name", + "Ok;Cancel", preset_name.data(), 255, NULL); + if (button == 1) { + state.AppendPresetFile(preset_name.data()); + } + if ((button == 0) || (button == 1) || (button == 2)) { + save_window = false; + TextCopy(preset_name.data(), "\0"); + } +} + +auto Gui::DrawMainMenu() -> void { + menu_grid.UpdateBounds(0, 0, GetScreenWidth(), MENU_HEIGHT); + + DrawMenuHeader(GRAY); + DrawGraphInfo(ORANGE); + DrawGraphControls(RED); + DrawCameraControls(DARKGREEN); + + if (input.editing) { + DrawEditControls(PURPLE); + } else { + DrawPuzzleControls(BLUE); + } + + DrawMenuFooter(GRAY); +} + +auto Gui::DrawPuzzleBoard() -> void { + board_grid.UpdateBounds( + 0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, + state.current_state.width, state.current_state.height); + + // Draw outer border + Rectangle bounds = board_grid.SquareBounds(); + DrawRectangleRec(bounds, state.current_state.IsWon() + ? BOARD_COLOR_WON + : BOARD_COLOR_RESTRICTED); + + // Draw inner borders + DrawRectangle(bounds.x + BOARD_PADDING, bounds.y + BOARD_PADDING, + bounds.width - 2 * BOARD_PADDING, + bounds.height - 2 * BOARD_PADDING, + state.current_state.restricted ? BOARD_COLOR_RESTRICTED + : BOARD_COLOR_FREE); + + // Draw target opening + if (state.current_state.HasWinCondition()) { + int target_x = state.current_state.target_x; + int target_y = state.current_state.target_y; + Block target_block = state.current_state.GetTargetBlock(); + Rectangle target_bounds = board_grid.SquareBounds( + target_x, target_y, target_block.width, target_block.height); + + Color opening_color = Fade( + state.current_state.IsWon() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED, + 0.3); + + if (target_x == 0) { + + // Left opening + DrawRectangle(target_bounds.x - BOARD_PADDING, target_bounds.y, + BOARD_PADDING, target_bounds.height, RAYWHITE); + DrawRectangle(target_bounds.x - BOARD_PADDING, target_bounds.y, + BOARD_PADDING, target_bounds.height, opening_color); + } + if (target_x + target_block.width == state.current_state.width) { + + // Right opening + DrawRectangle(target_bounds.x + target_bounds.width, target_bounds.y, + BOARD_PADDING, target_bounds.height, RAYWHITE); + DrawRectangle(target_bounds.x + target_bounds.width, target_bounds.y, + BOARD_PADDING, target_bounds.height, opening_color); + } + if (target_y == 0) { + + // Top opening + DrawRectangle(target_bounds.x, target_bounds.y - BOARD_PADDING, + target_bounds.width, BOARD_PADDING, RAYWHITE); + DrawRectangle(target_bounds.x, target_bounds.y - BOARD_PADDING, + target_bounds.width, BOARD_PADDING, opening_color); + } + if (target_y + target_block.height == state.current_state.height) { + + // Bottom opening + DrawRectangle(target_bounds.x, target_bounds.y + target_bounds.height, + target_bounds.width, BOARD_PADDING, RAYWHITE); + DrawRectangle(target_bounds.x, target_bounds.y + target_bounds.height, + target_bounds.width, BOARD_PADDING, opening_color); + } + } + + // Draw empty cells. Also set hovered blocks + input.hov_x = -1; + input.hov_y = -1; + for (int x = 0; x < board_grid.columns; ++x) { + for (int y = 0; y < board_grid.rows; ++y) { + DrawRectangleRec(board_grid.SquareBounds(x, y, 1, 1), RAYWHITE); + + Rectangle hov_bounds = board_grid.SquareBounds(x, y, 1, 1); + hov_bounds.x -= BOARD_PADDING; + hov_bounds.y -= BOARD_PADDING; + hov_bounds.width += BOARD_PADDING; + hov_bounds.height += BOARD_PADDING; + if (CheckCollisionPointRec(GetMousePosition() - Vector2(0, MENU_HEIGHT), + hov_bounds)) { + input.hov_x = x; + input.hov_y = y; + } + } + } + + // Draw blocks + for (const Block &block : state.current_state) { + Color c = BLOCK_COLOR; + if (block.target) { + c = TARGET_BLOCK_COLOR; + } else if (block.immovable) { + c = WALL_COLOR; + } + + DrawBoardBlock(block.x, block.y, block.width, block.height, c, + !block.immovable); + } + + // Draw block placing + if (input.editing && input.has_block_add_xy) { + if (input.hov_x >= 0 && input.hov_x < state.current_state.width && + input.hov_y >= 0 && input.hov_y < state.current_state.height && + input.hov_x >= input.block_add_x && input.hov_y >= input.block_add_y) { + bool collides = false; + for (const Block &block : state.current_state) { + if (block.Collides(Block(input.block_add_x, input.block_add_y, + input.hov_x - input.block_add_x + 1, + input.hov_y - input.block_add_y + 1, false))) { + collides = true; + break; + } + } + if (!collides) { + DrawBoardBlock(input.block_add_x, input.block_add_y, + input.hov_x - input.block_add_x + 1, + input.hov_y - input.block_add_y + 1, PURPLE, false); + } + } + } +} + +auto Gui::DrawGraphOverlay(int fps, int ups) -> void { + graph_overlay_grid.UpdateBounds(GetScreenWidth() / 2, MENU_HEIGHT); + + DrawLabel(graph_overlay_grid.Bounds(0, 0, 1, 1), + std::format("Dist: {:0>7.2f}", camera.distance), BLACK); + DrawLabel(graph_overlay_grid.Bounds(0, 1, 1, 1), + std::format("FoV: {:0>6.2f}", camera.fov), BLACK); + DrawLabel(graph_overlay_grid.Bounds(0, 2, 1, 1), + std::format("FPS: {:0>3}", fps), LIME); + DrawLabel(graph_overlay_grid.Bounds(0, 3, 1, 1), + std::format("UPS: {:0>3}", ups), ORANGE); +} + +auto Gui::Update() -> void { input.disable = WindowOpen(); } diff --git a/src/input.cpp b/src/input.cpp index b9e6dc8..c6a51a2 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -2,6 +2,7 @@ #include "config.hpp" #include +#include #include #ifdef TRACY @@ -9,189 +10,474 @@ #include #endif -auto InputHandler::HandleMouseHover() -> void { - const int board_width = GetScreenWidth() / 2.0 - 2 * BOARD_PADDING; - const int board_height = GetScreenHeight() - MENU_HEIGHT - 2 * BOARD_PADDING; - int block_size = std::min(board_width / state.current_state.width, - board_height / state.current_state.height) - - 2 * BLOCK_PADDING; - int x_offset = (board_width - (block_size + 2 * BLOCK_PADDING) * - state.current_state.width) / - 2.0; - int y_offset = (board_height - (block_size + 2 * BLOCK_PADDING) * - state.current_state.height) / - 2.0; +auto InputHandler::InitHandlers() -> void { + // The order matters if multiple handlers are registered to the same key - Vector2 m = GetMousePosition(); - if (m.x < x_offset) { - hov_x = 100; - } else { - hov_x = (m.x - x_offset) / (block_size + 2 * BLOCK_PADDING); + RegisterGenericHandler(&InputHandler::CameraPan); + RegisterGenericHandler(&InputHandler::CameraRotate); + RegisterGenericHandler(&InputHandler::CameraZoom); + RegisterGenericHandler(&InputHandler::CameraFov); + RegisterGenericHandler(&InputHandler::MouseHover); + + RegisterMousePressedHandler(MOUSE_BUTTON_LEFT, &InputHandler::CameraStartPan); + RegisterMousePressedHandler(MOUSE_BUTTON_LEFT, &InputHandler::SelectBlock); + RegisterMousePressedHandler(MOUSE_BUTTON_LEFT, &InputHandler::AddBlock); + RegisterMousePressedHandler(MOUSE_BUTTON_LEFT, &InputHandler::StartAddBlock); + RegisterMousePressedHandler(MOUSE_BUTTON_RIGHT, + &InputHandler::CameraStartRotate); + RegisterMousePressedHandler(MOUSE_BUTTON_RIGHT, &InputHandler::RemoveBlock); + RegisterMousePressedHandler(MOUSE_BUTTON_RIGHT, &InputHandler::ClearAddBlock); + RegisterMousePressedHandler(MOUSE_BUTTON_MIDDLE, &InputHandler::PlaceGoal); + + RegisterMouseReleasedHandler(MOUSE_BUTTON_LEFT, &InputHandler::CameraStopPan); + RegisterMouseReleasedHandler(MOUSE_BUTTON_RIGHT, + &InputHandler::CameraStopRotate); + + RegisterKeyPressedHandler(KEY_W, &InputHandler::MoveBlockNor); + RegisterKeyPressedHandler(KEY_D, &InputHandler::MoveBlockEas); + RegisterKeyPressedHandler(KEY_S, &InputHandler::MoveBlockSou); + RegisterKeyPressedHandler(KEY_A, &InputHandler::MoveBlockWes); + RegisterKeyPressedHandler(KEY_P, &InputHandler::PrintState); + RegisterKeyPressedHandler(KEY_N, &InputHandler::PreviousPreset); + RegisterKeyPressedHandler(KEY_M, &InputHandler::NextPreset); + RegisterKeyPressedHandler(KEY_R, &InputHandler::ResetState); + RegisterKeyPressedHandler(KEY_G, &InputHandler::FillGraph); + RegisterKeyPressedHandler(KEY_C, &InputHandler::ClearGraph); + RegisterKeyPressedHandler(KEY_I, &InputHandler::ToggleMarkSolutions); + RegisterKeyPressedHandler(KEY_O, &InputHandler::ToggleConnectSolutions); + RegisterKeyPressedHandler(KEY_U, &InputHandler::ToggleMarkPath); + RegisterKeyPressedHandler(KEY_SPACE, &InputHandler::MakeOptimalMove); + RegisterKeyPressedHandler(KEY_V, &InputHandler::GoToWorstState); + RegisterKeyPressedHandler(KEY_B, &InputHandler::GoToNearestTarget); + RegisterKeyPressedHandler(KEY_BACKSPACE, &InputHandler::UndoLastMove); + RegisterKeyPressedHandler(KEY_F, &InputHandler::ToggleRestrictedMovement); + RegisterKeyPressedHandler(KEY_T, &InputHandler::ToggleTargetBlock); + RegisterKeyPressedHandler(KEY_Y, &InputHandler::ToggleWallBlock); + RegisterKeyPressedHandler(KEY_UP, &InputHandler::AddBoardRow); + RegisterKeyPressedHandler(KEY_RIGHT, &InputHandler::AddBoardColumn); + RegisterKeyPressedHandler(KEY_DOWN, &InputHandler::RemoveBoardRow); + RegisterKeyPressedHandler(KEY_LEFT, &InputHandler::RemoveBoardColumn); + RegisterKeyPressedHandler(KEY_TAB, &InputHandler::ToggleEditing); + RegisterKeyPressedHandler(KEY_L, &InputHandler::ToggleCameraLock); + RegisterKeyPressedHandler(KEY_LEFT_ALT, + &InputHandler::ToggleCameraProjection); + RegisterKeyPressedHandler(KEY_X, &InputHandler::ClearGoal); +} + +auto InputHandler::MouseInMenuPane() -> bool { return mouse.y < MENU_HEIGHT; } + +auto InputHandler::MouseInBoardPane() -> bool { + return mouse.x < GetScreenWidth() / 2.0 && mouse.y >= MENU_HEIGHT; +} + +auto InputHandler::MouseInGraphPane() -> bool { + return mouse.x >= GetScreenWidth() / 2.0 && mouse.y >= MENU_HEIGHT; +} + +auto InputHandler::MouseHover() -> void { + last_mouse = mouse; + mouse = GetMousePosition(); +} + +auto InputHandler::CameraStartPan() -> void { + if (!MouseInGraphPane()) { + return; } - if (m.y - MENU_HEIGHT < y_offset) { - hov_y = 100; - } else { - hov_y = (m.y - MENU_HEIGHT - y_offset) / (block_size + 2 * BLOCK_PADDING); + + camera_panning = true; + // Enable this if the camera should be pannable even when locked (releasing + // the lock in the process): + // camera_lock = false; +} + +auto InputHandler::CameraPan() -> void { + if (camera_panning) { + camera.Pan(last_mouse, mouse); } } -auto InputHandler::HandleMouse() -> void { - if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { - // If we clicked a block... - if (state.current_state.GetBlock(hov_x, hov_y).IsValid()) { - sel_x = hov_x; - sel_y = hov_y; - } - // If we clicked empty space... - else { - // Select a position - if (!has_block_add_xy) { - if (hov_x >= 0 && hov_x < state.current_state.width && hov_y >= 0 && - hov_y < state.current_state.height) { - block_add_x = hov_x; - block_add_y = hov_y; - has_block_add_xy = true; - } - } - // If we have already selected a position - else { - int block_add_width = hov_x - block_add_x + 1; - int block_add_height = hov_y - block_add_y + 1; - if (block_add_width <= 0 || block_add_height <= 0) { - block_add_x = -1; - block_add_y = -1; - has_block_add_xy = false; - } else if (block_add_x >= 0 && - block_add_x + block_add_width <= state.current_state.width && - block_add_y >= 0 && - block_add_y + block_add_height <= - state.current_state.height) { - bool success = state.current_state.AddBlock( - Block(block_add_x, block_add_y, block_add_width, block_add_height, - false)); +auto InputHandler::CameraStopPan() -> void { camera_panning = false; } - if (success) { - block_add_x = -1; - block_add_y = -1; - has_block_add_xy = false; - state.ClearGraph(); - state.edited = true; - } - } - } - } - } else if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) { - if (state.current_state.RemoveBlock(hov_x, hov_y)) { - state.ClearGraph(); - state.edited = true; - } else if (has_block_add_xy) { +auto InputHandler::CameraStartRotate() -> void { + if (!MouseInGraphPane()) { + return; + } + + camera_rotating = true; +} + +auto InputHandler::CameraRotate() -> void { + if (camera_rotating) { + camera.Rotate(last_mouse, mouse); + } +} + +auto InputHandler::CameraStopRotate() -> void { camera_rotating = false; } + +auto InputHandler::CameraZoom() -> void { + if (!MouseInGraphPane() || IsKeyDown(KEY_LEFT_CONTROL)) { + return; + } + + float wheel = GetMouseWheelMove(); + + if (IsKeyDown(KEY_LEFT_SHIFT)) { + camera.distance -= wheel * ZOOM_SPEED * ZOOM_MULTIPLIER; + } else { + camera.distance -= wheel * ZOOM_SPEED; + } +} + +auto InputHandler::CameraFov() -> void { + if (!MouseInGraphPane() || !IsKeyDown(KEY_LEFT_CONTROL) || + IsKeyDown(KEY_LEFT_SHIFT)) { + return; + } + + float wheel = GetMouseWheelMove(); + camera.fov -= wheel * FOV_SPEED; +} + +auto InputHandler::ToggleCameraLock() -> void { + if (!camera_lock) { + camera_panning = false; + } + + camera_lock = !camera_lock; +} + +auto InputHandler::ToggleCameraProjection() -> void { + camera.projection = camera.projection == CAMERA_PERSPECTIVE + ? CAMERA_ORTHOGRAPHIC + : CAMERA_PERSPECTIVE; +} + +auto InputHandler::SelectBlock() -> void { + if (state.current_state.GetBlock(hov_x, hov_y).IsValid()) { + sel_x = hov_x; + sel_y = hov_y; + } +} + +auto InputHandler::StartAddBlock() -> void { + if (!editing || state.current_state.GetBlock(hov_x, hov_y).IsValid() || + has_block_add_xy) { + return; + } + + if (hov_x >= 0 && hov_x < state.current_state.width && hov_y >= 0 && + hov_y < state.current_state.height) { + block_add_x = hov_x; + block_add_y = hov_y; + has_block_add_xy = true; + } +} + +auto InputHandler::ClearAddBlock() -> void { + if (!editing || !has_block_add_xy) { + return; + } + + block_add_x = -1; + block_add_y = -1; + has_block_add_xy = false; +} + +auto InputHandler::AddBlock() -> void { + if (!editing || state.current_state.GetBlock(hov_x, hov_y).IsValid() || + !has_block_add_xy) { + return; + } + + int block_add_width = hov_x - block_add_x + 1; + int block_add_height = hov_y - block_add_y + 1; + if (block_add_width <= 0 || block_add_height <= 0) { + block_add_x = -1; + block_add_y = -1; + has_block_add_xy = false; + } else if (block_add_x >= 0 && + block_add_x + block_add_width <= state.current_state.width && + block_add_y >= 0 && + block_add_y + block_add_height <= state.current_state.height) { + bool success = state.current_state.AddBlock(Block( + block_add_x, block_add_y, block_add_width, block_add_height, false)); + + if (success) { + sel_x = block_add_x; + sel_y = block_add_y; block_add_x = -1; block_add_y = -1; has_block_add_xy = false; - } - } else if (IsMouseButtonPressed(MOUSE_BUTTON_MIDDLE)) { - if (hov_x >= 0 && hov_x < state.current_state.width && hov_y >= 0 && - hov_y < state.current_state.height) { - if (state.current_state.SetGoal(hov_x, hov_y)) { - // We can't just call state.FindWinningStates() because the - // state is entirely different if it has a different win condition. - state.ClearGraph(); - } + state.ClearGraph(); + state.edited = true; } } } -auto InputHandler::HandleKeys() -> void { - if (IsKeyPressed(KEY_W)) { - if (state.current_state.MoveBlockAt(sel_x, sel_y, Direction::NOR)) { - sel_y--; - } - } else if (IsKeyPressed(KEY_A)) { - if (state.current_state.MoveBlockAt(sel_x, sel_y, Direction::WES)) { - sel_x--; - } - } else if (IsKeyPressed(KEY_S)) { - if (state.current_state.MoveBlockAt(sel_x, sel_y, Direction::SOU)) { - sel_y++; - } - } else if (IsKeyPressed(KEY_D)) { - if (state.current_state.MoveBlockAt(sel_x, sel_y, Direction::EAS)) { - sel_x++; - } - } else if (IsKeyPressed(KEY_P)) { - std::cout << "State: " << state.current_state.state << std::endl; - Block sel = state.current_state.GetBlock(sel_x, sel_y); - int idx = state.current_state.GetIndex(sel.x, sel.y) - 5; - if (sel.IsValid()) { - std::cout << "Sel: " << state.current_state.state.substr(0, 5) - << std::string(idx, '.') << sel.ToString() - << std::string(state.current_state.state.length() - idx - 7, - '.') - << std::endl; - } - } else if (IsKeyPressed(KEY_N)) { - block_add_x = -1; - block_add_y = -1; - has_block_add_xy = false; - state.PreviousPreset(); - } else if (IsKeyPressed(KEY_M)) { - block_add_x = -1; - block_add_y = -1; - has_block_add_xy = false; - state.NextPreset(); - } else if (IsKeyPressed(KEY_R)) { - state.ResetState(); - } else if (IsKeyPressed(KEY_G)) { - state.FillGraph(); - } else if (IsKeyPressed(KEY_C)) { - state.ClearGraph(); - } else if (IsKeyPressed(KEY_I)) { - mark_solutions = !mark_solutions; - } else if (IsKeyPressed(KEY_O)) { - connect_solutions = !connect_solutions; - } else if (IsKeyPressed(KEY_U)) { - mark_path = !mark_path; - } else if (IsKeyPressed(KEY_SPACE)) { - state.NextPath(); - } else if (IsKeyPressed(KEY_V)) { - state.GoToWorst(); - } else if (IsKeyPressed(KEY_B)) { - state.GoToNearestTarget(); - } else if (IsKeyPressed(KEY_BACKSPACE)) { - state.PopHistory(); - } else if (IsKeyPressed(KEY_F)) { - state.current_state.ToggleRestricted(); - state.ClearGraph(); - state.edited = true; - } else if (IsKeyPressed(KEY_T)) { - state.current_state.ToggleTarget(sel_x, sel_y); - state.ClearGraph(); - state.edited = true; - } else if (IsKeyPressed(KEY_Y)) { - state.current_state.ToggleWall(sel_x, sel_y); - state.ClearGraph(); - state.edited = true; - } else if (IsKeyPressed(KEY_LEFT) && state.current_state.width > 1) { - state.current_state = state.current_state.RemoveColumn(); - state.ClearGraph(); - state.edited = true; - } else if (IsKeyPressed(KEY_RIGHT) && state.current_state.width < 9) { - state.current_state = state.current_state.AddColumn(); - state.ClearGraph(); - state.edited = true; - } else if (IsKeyPressed(KEY_UP) && state.current_state.height > 1) { - state.current_state = state.current_state.RemoveRow(); - state.ClearGraph(); - state.edited = true; - } else if (IsKeyPressed(KEY_DOWN) && state.current_state.height < 9) { - state.current_state = state.current_state.AddRow(); - state.ClearGraph(); - state.edited = true; +auto InputHandler::RemoveBlock() -> void { + Block block = state.current_state.GetBlock(hov_x, hov_y); + if (!editing || has_block_add_xy || !block.IsValid() || + !state.current_state.RemoveBlock(hov_x, hov_y)) { + return; } + + if (block.Covers(sel_x, sel_y)) { + sel_x = 0; + sel_y = 0; + } + + state.ClearGraph(); + state.edited = true; +} + +auto InputHandler::PlaceGoal() -> void { + if (!editing || hov_x < 0 || hov_x >= state.current_state.width || + hov_y < 0 || hov_y >= state.current_state.height) { + return; + } + + if (state.current_state.SetGoal(hov_x, hov_y)) { + // We can't just call state.FindWinningStates() because the + // state is entirely different if it has a different win condition. + state.ClearGraph(); + } +} + +auto InputHandler::MoveBlockNor() -> void { + if (state.current_state.MoveBlockAt(sel_x, sel_y, Direction::NOR)) { + sel_y--; + } +} + +auto InputHandler::MoveBlockEas() -> void { + if (state.current_state.MoveBlockAt(sel_x, sel_y, Direction::EAS)) { + sel_x++; + } +} + +auto InputHandler::MoveBlockSou() -> void { + if (state.current_state.MoveBlockAt(sel_x, sel_y, Direction::SOU)) { + sel_y++; + } +} + +auto InputHandler::MoveBlockWes() -> void { + if (state.current_state.MoveBlockAt(sel_x, sel_y, Direction::WES)) { + sel_x--; + } +} + +auto InputHandler::PrintState() const -> void { + std::println("State: \"{}\"", state.current_state.state); + Block sel = state.current_state.GetBlock(sel_x, sel_y); + int idx = state.current_state.GetIndex(sel.x, sel.y) - State::prefix; + if (sel.IsValid()) { + std::println("Sel: \"{}{}{}{}\"", + state.current_state.state.substr(0, State::prefix), + std::string(idx, '.'), sel.ToString(), + std::string(state.current_state.state.length() - idx - + State::prefix - 2, + '.')); + } +} + +auto InputHandler::PreviousPreset() -> void { + if (editing) { + return; + } + + block_add_x = -1; + block_add_y = -1; + has_block_add_xy = false; + state.PreviousPreset(); +} + +auto InputHandler::NextPreset() -> void { + if (editing) { + return; + } + + block_add_x = -1; + block_add_y = -1; + has_block_add_xy = false; + state.NextPreset(); +} + +auto InputHandler::ResetState() -> void { + if (editing) { + return; + } + + state.ResetState(); +} + +auto InputHandler::FillGraph() -> void { state.FillGraph(); } + +auto InputHandler::ClearGraph() -> void { state.ClearGraph(); } + +auto InputHandler::ToggleMarkSolutions() -> void { + mark_solutions = !mark_solutions; +} + +auto InputHandler::ToggleConnectSolutions() -> void { + connect_solutions = !connect_solutions; +} + +auto InputHandler::ToggleMarkPath() -> void { mark_path = !mark_path; } + +auto InputHandler::MakeOptimalMove() -> void { state.NextPath(); } + +auto InputHandler::GoToWorstState() -> void { state.GoToWorst(); } + +auto InputHandler::GoToNearestTarget() -> void { state.GoToNearestTarget(); } + +auto InputHandler::UndoLastMove() -> void { state.PopHistory(); } + +auto InputHandler::ToggleRestrictedMovement() -> void { + if (!editing) { + return; + } + + state.current_state.ToggleRestricted(); + state.ClearGraph(); + state.edited = true; +} + +auto InputHandler::ToggleTargetBlock() -> void { + if (!editing) { + return; + } + + state.current_state.ToggleTarget(sel_x, sel_y); + state.ClearGraph(); + state.edited = true; +} + +auto InputHandler::ToggleWallBlock() -> void { + if (!editing) { + return; + } + + state.current_state.ToggleWall(sel_x, sel_y); + state.ClearGraph(); + state.edited = true; +} + +auto InputHandler::RemoveBoardColumn() -> void { + if (!editing || state.current_state.width <= 1) { + return; + } + + state.current_state = state.current_state.RemoveColumn(); + state.ClearGraph(); + state.edited = true; +} + +auto InputHandler::AddBoardColumn() -> void { + if (!editing || state.current_state.width >= 9) { + return; + } + + state.current_state = state.current_state.AddColumn(); + state.ClearGraph(); + state.edited = true; +} + +auto InputHandler::RemoveBoardRow() -> void { + if (!editing || state.current_state.height <= 1) { + return; + } + + state.current_state = state.current_state.RemoveRow(); + state.ClearGraph(); + state.edited = true; +} + +auto InputHandler::AddBoardRow() -> void { + if (!editing || state.current_state.height >= 9) { + return; + } + + state.current_state = state.current_state.AddRow(); + state.ClearGraph(); + state.edited = true; +} + +auto InputHandler::ToggleEditing() -> void { + if (editing) { + has_block_add_xy = false; + block_add_x = -1; + block_add_y = -1; + } + + editing = !editing; +} + +auto InputHandler::ClearGoal() -> void { + if (!editing) { + return; + } + + state.current_state.ClearGoal(); + state.ClearGraph(); + state.edited = true; +} + +auto InputHandler::RegisterGenericHandler( + std::function handler) -> void { + generic_handlers.push_back({handler}); +} + +auto InputHandler::RegisterMousePressedHandler( + MouseButton button, std::function handler) -> void { + mouse_pressed_handlers.push_back({handler, button}); +} + +auto InputHandler::RegisterMouseReleasedHandler( + MouseButton button, std::function handler) -> void { + mouse_released_handlers.push_back({handler, button}); +} + +auto InputHandler::RegisterKeyPressedHandler( + KeyboardKey key, std::function handler) -> void { + key_pressed_handlers.push_back({handler, key}); +} + +auto InputHandler::RegisterKeyReleasedHandler( + KeyboardKey key, std::function handler) -> void { + key_released_handlers.push_back({handler, key}); } auto InputHandler::HandleInput() -> void { - HandleMouseHover(); - HandleMouse(); - HandleKeys(); + if (disable) { + return; + } + + for (const GenericHandler &handler : generic_handlers) { + handler.handler(*this); + } + + for (const MouseHandler &handler : mouse_pressed_handlers) { + if (IsMouseButtonPressed(handler.button)) { + handler.handler(*this); + } + } + + for (const MouseHandler &handler : mouse_released_handlers) { + if (IsMouseButtonReleased(handler.button)) { + handler.handler(*this); + } + } + + for (const KeyboardHandler &handler : key_pressed_handlers) { + if (IsKeyPressed(handler.key)) { + handler.handler(*this); + } + } + + for (const KeyboardHandler &handler : key_released_handlers) { + if (IsKeyReleased(handler.key)) { + handler.handler(*this); + } + } } diff --git a/src/main.cpp b/src/main.cpp index e1180b6..eecc650 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,10 @@ +#include #include #include #include #include "config.hpp" +#include "gui.hpp" #include "input.hpp" #include "physics.hpp" #include "renderer.hpp" @@ -27,7 +29,7 @@ auto main(int argc, char *argv[]) -> int { // RayLib window setup SetTraceLogLevel(LOG_ERROR); - // SetConfigFlags(FLAG_VSYNC_HINT); + SetConfigFlags(FLAG_VSYNC_HINT); SetConfigFlags(FLAG_MSAA_4X_HINT); SetConfigFlags(FLAG_WINDOW_RESIZABLE); SetConfigFlags(FLAG_WINDOW_ALWAYS_RUN); @@ -36,11 +38,17 @@ auto main(int argc, char *argv[]) -> int { // Game setup ThreadedPhysics physics; StateManager state(physics, preset_file); - InputHandler input(state); OrbitCamera3D camera; - Renderer renderer(camera, state, input); + InputHandler input(state, camera); + Gui gui(input, state, camera); + Renderer renderer(camera, state, input, gui); - unsigned int ups; + std::chrono::time_point last = std::chrono::high_resolution_clock::now(); + std::chrono::duration fps_accumulator(0); + unsigned int loop_iterations = 0; + + unsigned int fps = 0; + unsigned int ups = 0; // Read from physics std::vector masses; // Read from physics // Game loop @@ -49,6 +57,12 @@ auto main(int argc, char *argv[]) -> int { FrameMarkStart("MainThread"); #endif + // Time tracking + std::chrono::time_point now = std::chrono::high_resolution_clock::now(); + std::chrono::duration deltatime = now - last; + fps_accumulator += deltatime; + last = now; + // Input update input.HandleInput(); state.UpdateGraph(); // Add state added after user input @@ -86,7 +100,7 @@ auto main(int argc, char *argv[]) -> int { std::size_t current_index = state.CurrentMassIndex(); if (masses.size() > current_index) { const Mass ¤t_mass = masses.at(current_index); - camera.Update(current_mass.position); + camera.Update(current_mass.position, input.camera_lock); } // Rendering @@ -94,7 +108,16 @@ auto main(int argc, char *argv[]) -> int { renderer.DrawMassSprings(masses); renderer.DrawKlotski(); renderer.DrawMenu(masses); - renderer.DrawTextures(ups); + renderer.DrawTextures(fps, ups); + + if (fps_accumulator.count() > 1.0) { + // Update each second + fps = loop_iterations; + loop_iterations = 0; + fps_accumulator = std::chrono::duration(0); + } + ++loop_iterations; + #ifdef TRACY FrameMark; FrameMarkEnd("MainThread"); diff --git a/src/octree.cpp b/src/octree.cpp index c2dd790..6ac4580 100644 --- a/src/octree.cpp +++ b/src/octree.cpp @@ -1,8 +1,6 @@ #include "octree.hpp" #include "config.hpp" -#include "util.hpp" -#include #include #ifdef TRACY @@ -168,11 +166,3 @@ auto Octree::CalculateForce(int node_idx, const Vector3 &pos) const -> Vector3 { return force; } - -auto Octree::Print() const -> void { - std::cout << "Octree Start ===========================" << std::endl; - for (const auto &node : nodes) { - std::cout << "Center: " << node.mass_center << ", Mass: " << node.mass_total - << ", Direct Children: " << node.ChildCount() << std::endl; - } -} diff --git a/src/physics.cpp b/src/physics.cpp index 648809a..1f328c0 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -212,9 +212,9 @@ auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state) }; std::chrono::time_point last = std::chrono::high_resolution_clock::now(); - std::chrono::duration accumulator(0); - std::chrono::duration update_accumulator(0); - unsigned int updates = 0; + std::chrono::duration physics_accumulator(0); + std::chrono::duration ups_accumulator(0); + unsigned int loop_iterations = 0; while (state.running.load()) { #ifdef TRACY @@ -224,8 +224,8 @@ auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state) // Time tracking std::chrono::time_point now = std::chrono::high_resolution_clock::now(); std::chrono::duration deltatime = now - last; - accumulator += deltatime; - update_accumulator += deltatime; + physics_accumulator += deltatime; + ups_accumulator += deltatime; last = now; // Handle queued commands @@ -248,14 +248,14 @@ auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state) } // Physics update - if (accumulator.count() > TIMESTEP) { + if (physics_accumulator.count() > TIMESTEP) { mass_springs.ClearForces(); mass_springs.CalculateSpringForces(); mass_springs.CalculateRepulsionForces(); mass_springs.VerletUpdate(TIMESTEP * SIM_SPEED); - ++updates; - accumulator -= std::chrono::duration(TIMESTEP); + ++loop_iterations; + physics_accumulator -= std::chrono::duration(TIMESTEP); } // Publish the positions for the renderer (copy) @@ -275,11 +275,11 @@ auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state) break; } - if (update_accumulator.count() > 1.0) { + if (ups_accumulator.count() > 1.0) { // Update each second - state.ups = updates; - updates = 0; - update_accumulator = std::chrono::duration(0); + state.ups = loop_iterations; + loop_iterations = 0; + ups_accumulator = std::chrono::duration(0); } state.masses.clear(); diff --git a/src/puzzle.cpp b/src/puzzle.cpp index 028e987..0374132 100644 --- a/src/puzzle.cpp +++ b/src/puzzle.cpp @@ -1,6 +1,7 @@ #include "puzzle.hpp" #include "config.hpp" +#include #include #ifdef TRACY @@ -8,7 +9,7 @@ #include #endif -auto Block::Hash() const -> int { +auto Block::Hash() const -> std::size_t { std::string s = std::format("{},{},{},{}", x, y, width, height); return std::hash{}(s); } @@ -59,7 +60,9 @@ auto Block::Collides(const Block &other) const -> bool { y < other.y + other.height && y + height > other.y; } -auto State::Hash() const -> int { return std::hash{}(state); } +auto State::Hash() const -> std::size_t { + return std::hash{}(state); +} auto State::HasWinCondition() const -> bool { return target_x != 9 && target_y != 9; @@ -80,8 +83,9 @@ auto State::IsWon() const -> bool { } auto State::SetGoal(int x, int y) -> bool { - if (x < 0 || x >= width || y < 0 || y >= height || - !GetTargetBlock().IsValid()) { + Block target_block = GetTargetBlock(); + if (!target_block.IsValid() || x < 0 || x + target_block.width > width || + y < 0 || y + target_block.height > height) { return false; } @@ -99,6 +103,12 @@ auto State::SetGoal(int x, int y) -> bool { return true; } +auto State::ClearGoal() -> void { + target_x = 9; + target_y = 9; + state.replace(3, 2, "99"); +} + auto State::AddColumn() const -> State { State newstate = State(width + 1, height, restricted); @@ -379,8 +389,8 @@ auto State::Closure() const } } while (remaining_states.size() > 0); - std::cout << "Closure contains " << states.size() << " states with " - << links.size() << " links." << std::endl; + std::println("State space has size {} with {} transitions.", states.size(), + links.size()); return std::make_pair(states, links); } diff --git a/src/renderer.cpp b/src/renderer.cpp index 2bd2b48..b8cebfe 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -3,7 +3,6 @@ #include "puzzle.hpp" #include -#include #include #include #include @@ -143,7 +142,7 @@ auto Renderer::DrawMassSprings(const std::vector &masses) -> void { } // Mark visited states - for (const State &_state : state.visited_states) { + for (const auto &[_state, visits] : state.visited_states) { std::size_t visited_index = state.states.at(_state); if (masses.size() > visited_index) { @@ -180,13 +179,7 @@ auto Renderer::DrawMassSprings(const std::vector &masses) -> void { BLUE); } - // DrawCubeWires(current_mass.position, REPULSION_RANGE, REPULSION_RANGE, - // REPULSION_RANGE, BLACK); - // DrawGrid(100, 1.0); - // DrawSphere(camera.target, VERTEX_SIZE, ORANGE); EndMode3D(); - - DrawLine(0, 0, 0, GetScreenHeight() - MENU_HEIGHT, BLACK); EndTextureMode(); } @@ -198,116 +191,8 @@ auto Renderer::DrawKlotski() -> void { BeginTextureMode(klotski_target); ClearBackground(RAYWHITE); - // Draw Board - const int board_width = GetScreenWidth() / 2 - 2 * BOARD_PADDING; - const int board_height = GetScreenHeight() - MENU_HEIGHT - 2 * BOARD_PADDING; - int block_size = std::min(board_width / state.current_state.width, - board_height / state.current_state.height) - - 2 * BLOCK_PADDING; - int x_offset = (board_width - (block_size + 2 * BLOCK_PADDING) * - state.current_state.width) / - 2.0; - int y_offset = (board_height - (block_size + 2 * BLOCK_PADDING) * - state.current_state.height) / - 2.0; + gui.DrawPuzzleBoard(); - DrawRectangle(0, 0, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, - RAYWHITE); - DrawRectangle(x_offset, y_offset, - board_width - 2 * x_offset + 2 * BOARD_PADDING, - board_height - 2 * y_offset + 2 * BOARD_PADDING, - state.current_state.IsWon() - ? GREEN - : (state.current_state.restricted ? DARKGRAY : LIGHTGRAY)); - for (int x = 0; x < state.current_state.width; ++x) { - for (int y = 0; y < state.current_state.height; ++y) { - DrawRectangle(x_offset + BOARD_PADDING + x * BLOCK_PADDING * 2 + - BLOCK_PADDING + x * block_size, - y_offset + BOARD_PADDING + y * BLOCK_PADDING * 2 + - BLOCK_PADDING + y * block_size, - block_size, block_size, WHITE); - } - } - - // Draw Blocks - for (Block block : state.current_state) { - Color c = BLOCK_COLOR; - if (block.Covers(input.sel_x, input.sel_y)) { - c = HL_BLOCK_COLOR; - } - if (block.target) { - if (block.Covers(input.sel_x, input.sel_y)) { - c = HL_TARGET_BLOCK_COLOR; - } else { - c = TARGET_BLOCK_COLOR; - } - } else if (block.immovable) { - if (block.Covers(input.sel_x, input.sel_y)) { - c = HL_WALL_COLOR; - } else { - c = WALL_COLOR; - } - } - DrawRectangle(x_offset + BOARD_PADDING + block.x * BLOCK_PADDING * 2 + - BLOCK_PADDING + block.x * block_size, - y_offset + BOARD_PADDING + block.y * BLOCK_PADDING * 2 + - BLOCK_PADDING + block.y * block_size, - block.width * block_size + block.width * 2 * BLOCK_PADDING - - 2 * BLOCK_PADDING, - block.height * block_size + block.height * 2 * BLOCK_PADDING - - 2 * BLOCK_PADDING, - c); - - if (block.Covers(input.hov_x, input.hov_y)) { - DrawRectangleLinesEx( - Rectangle(x_offset + BOARD_PADDING + block.x * BLOCK_PADDING * 2 + - BLOCK_PADDING + block.x * block_size, - y_offset + BOARD_PADDING + block.y * BLOCK_PADDING * 2 + - BLOCK_PADDING + block.y * block_size, - block.width * block_size + block.width * 2 * BLOCK_PADDING - - 2 * BLOCK_PADDING, - block.height * block_size + - block.height * 2 * BLOCK_PADDING - 2 * BLOCK_PADDING), - 2.0, BLACK); - } - } - - // Draw editing starting position - if (input.block_add_x >= 0 && input.block_add_y >= 0 && - input.hov_x >= input.block_add_x && input.hov_y >= input.block_add_y) { - int block_width = input.hov_x - input.block_add_x + 1; - int block_height = input.hov_y - input.block_add_y + 1; - DrawRectangle( - x_offset + BOARD_PADDING + input.block_add_x * BLOCK_PADDING * 2 + - BLOCK_PADDING + input.block_add_x * block_size, - y_offset + BOARD_PADDING + input.block_add_y * BLOCK_PADDING * 2 + - BLOCK_PADDING + input.block_add_y * block_size, - block_width * block_size + block_width * 2 * BLOCK_PADDING - - 2 * BLOCK_PADDING, - block_height * block_size + block_height * 2 * BLOCK_PADDING - - 2 * BLOCK_PADDING, - Fade(BLOCK_COLOR, 0.5)); - } - - // Draw board goal position - const Block target = state.current_state.GetTargetBlock(); - if (target.IsValid() && state.current_state.HasWinCondition()) { - int target_x = state.current_state.target_x; - int target_y = state.current_state.target_y; - DrawRectangleLinesEx( - Rectangle(x_offset + BOARD_PADDING + target_x * BLOCK_PADDING * 2 + - BLOCK_PADDING + target_x * block_size, - y_offset + BOARD_PADDING + target_y * BLOCK_PADDING * 2 + - BLOCK_PADDING + target_y * block_size, - target.width * block_size + target.width * 2 * BLOCK_PADDING - - 2 * BLOCK_PADDING, - target.height * block_size + - target.height * 2 * BLOCK_PADDING - 2 * BLOCK_PADDING), - 2.0, TARGET_BLOCK_COLOR); - } - - DrawLine(GetScreenWidth() / 2 - 1, 0, GetScreenWidth() / 2 - 1, - GetScreenHeight() - MENU_HEIGHT, BLACK); EndTextureMode(); } @@ -319,87 +204,14 @@ auto Renderer::DrawMenu(const std::vector &masses) -> void { BeginTextureMode(menu_target); ClearBackground(RAYWHITE); - float btn_width = - static_cast(GetScreenWidth() - (MENU_COLS * MENU_PAD + MENU_PAD)) / - MENU_COLS; - float btn_height = - static_cast(MENU_HEIGHT - (MENU_ROWS * MENU_PAD + MENU_PAD)) / - MENU_ROWS; + gui.DrawMainMenu(); - auto draw_btn = [&](int x, int y, std::string text, Color color) { - int posx = MENU_PAD + x * (MENU_PAD + btn_width); - int posy = MENU_PAD + (y + 1) * (MENU_PAD + btn_height); - DrawRectangle(posx, posy, btn_width, btn_height, Fade(color, 0.7)); - DrawRectangleLines(posx, posy, btn_width, btn_height, color); - DrawText(text.data(), posx + BUTTON_PAD, posy + BUTTON_PAD, - btn_height - 2.0 * BUTTON_PAD, WHITE); - }; - - auto draw_subtitle = [&](std::string text, Color color) { - int posx = MENU_PAD; - int posy = MENU_PAD; - DrawRectangle(posx, posy, - btn_width * MENU_COLS + MENU_PAD * (MENU_COLS - 1), - btn_height, Fade(color, 0.7)); - DrawRectangleLines(posx, posy, - btn_width * MENU_COLS + MENU_PAD * (MENU_COLS - 1), - btn_height, color); - DrawText(text.data(), posx + BUTTON_PAD, posy + BUTTON_PAD, - btn_height = 2.0 * BUTTON_PAD, WHITE); - }; - - // Left column - draw_btn(0, 0, - std::format("States: {} / Transitions: {} / Winning: {}", - masses.size(), state.springs.size(), - state.winning_states.size()), - ORANGE); - draw_btn(0, 1, - std::format("Preset (M/N) / {} (F)", - state.current_state.restricted ? "Restricted" : "Free"), - ORANGE); - draw_btn(0, 2, std::format("Pan (LMB) / Rotate (RMB) / Zoom (Wheel)"), - DARKGREEN); - draw_btn( - 0, 3, - std::format("Lock Camera to Current State (L): {}", camera.target_lock), - DARKGREEN); - - // Center column - draw_btn(1, 0, std::format("Select (LMB) / Move (WASD) / Target/Wall (T/Y)"), - DARKBLUE); - draw_btn(1, 1, std::format("Add/Remove Col/Row (Arrow Keys)"), DARKBLUE); - draw_btn(1, 2, std::format("Add/Remove Block (LMB/RMB) / Set Goal (MMB)"), - DARKBLUE); - draw_btn(1, 3, std::format("Print State (P) / Reset State (R)"), DARKBLUE); - - // Right column - draw_btn(2, 0, std::format("Populate Graph (G) / Clear Graph (C)"), - DARKPURPLE); - draw_btn(2, 1, - std::format("Path (U): {} / Goals (I): {} / Lines (O): {}", - input.mark_path, input.mark_solutions, - input.connect_solutions), - DARKPURPLE); - draw_btn(2, 2, std::format("Best move (Space) / Move back (Backspace)"), - DARKPURPLE); - draw_btn(2, 3, - std::format("Worst (V) / Target (B) / Distance: {}", - state.winning_path.size() > 0 - ? state.winning_path.size() - 1 - : 0), - DARKPURPLE); - - draw_subtitle(std::format("Puzzle {}: {}", state.current_preset + 1, - state.comments.at(state.current_preset)), - BLACK); - - DrawLine(0, MENU_HEIGHT - 1, GetScreenWidth(), MENU_HEIGHT - 1, BLACK); EndTextureMode(); } -auto Renderer::DrawTextures(float ups) -> void { +auto Renderer::DrawTextures(int fps, int ups) -> void { BeginDrawing(); + DrawTextureRec(menu_target.texture, Rectangle(0, 0, menu_target.texture.width, -1 * menu_target.texture.height), @@ -413,8 +225,20 @@ auto Renderer::DrawTextures(float ups) -> void { -1 * render_target.texture.height), Vector2(GetScreenWidth() / 2.0, MENU_HEIGHT), WHITE); - DrawFPS(GetScreenWidth() / 2 + 10, MENU_HEIGHT + 10); - DrawText(TextFormat("%.0f UPS", ups), GetScreenWidth() / 2 + 120, - MENU_HEIGHT + 10, 20, ORANGE); + // Draw borders + DrawRectangleLinesEx(Rectangle(0, 0, GetScreenWidth(), MENU_HEIGHT), 1.0, + BLACK); + DrawRectangleLinesEx(Rectangle(0, MENU_HEIGHT, GetScreenWidth() / 2.0, + GetScreenHeight() - MENU_HEIGHT), + 1.0, BLACK); + DrawRectangleLinesEx(Rectangle(GetScreenWidth() / 2.0, MENU_HEIGHT, + GetScreenWidth() / 2.0, + GetScreenHeight() - MENU_HEIGHT), + 1.0, BLACK); + + gui.DrawGraphOverlay(fps, ups); + gui.DrawSavePresetPopup(); + gui.Update(); + EndDrawing(); } diff --git a/src/state.cpp b/src/state.cpp index 8494c03..0aaf776 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #ifdef TRACY @@ -11,12 +12,13 @@ #include #endif -auto StateManager::ParsePresetFile(const std::string &preset_file) -> void { +auto StateManager::ParsePresetFile(const std::string &_preset_file) -> bool { + preset_file = _preset_file; + std::ifstream file(preset_file); if (!file) { - std::cout << "Preset file \"" << preset_file << "\" couldn't be loaded." - << std::endl; - return; + std::println("Preset file \"{}\" couldn't be loaded.", preset_file); + return false; } std::string line; @@ -31,9 +33,8 @@ auto StateManager::ParsePresetFile(const std::string &preset_file) -> void { } if (preset_lines.size() == 0 || comment_lines.size() != preset_lines.size()) { - std::cout << "Preset file \"" << preset_file << "\" couldn't be loaded." - << std::endl; - return; + std::println("Preset file \"{}\" couldn't be loaded.", preset_file); + return false; } presets.clear(); @@ -42,7 +43,27 @@ auto StateManager::ParsePresetFile(const std::string &preset_file) -> void { } comments = comment_lines; - std::cout << "Loaded " << preset_lines.size() << " presets." << std::endl; + std::println("Loaded {} presets from \"{}\".", preset_lines.size(), + preset_file); + + return true; +} + +auto StateManager::AppendPresetFile(const std::string preset_name) -> void { + std::println("Saving preset \"{}\" to \"{}\"", preset_name, preset_file); + + std::ofstream file(preset_file, std::ios_base::app | std::ios_base::out); + if (!file) { + std::println("Preset file \"{}\" couldn't be loaded.", preset_file); + return; + } + + file << "\n# " << preset_name << "\n" << current_state.state << std::flush; + + std::println("Refreshing presets..."); + if (ParsePresetFile(preset_file)) { + LoadPreset(presets.size() - 1); + } } auto StateManager::LoadPreset(int preset) -> void { @@ -55,6 +76,11 @@ auto StateManager::LoadPreset(int preset) -> void { auto StateManager::ResetState() -> void { current_state = presets.at(current_preset); previous_state = current_state; + for (auto &[state, visits] : visited_states) { + visits = 0; + } + visited_states[current_state]++; + total_moves = 0; if (edited) { // We also need to clear the graph in case the state has been edited // because the graph could contain states that are impossible to reach @@ -83,8 +109,6 @@ auto StateManager::NextPath() -> void { } std::size_t parent = target_distances.parents[CurrentMassIndex()]; - // std::cout << "Parent of node " << CurrentMassIndex() << " is " << parent - // << std::endl; current_state = masses.at(parent); FindTargetPath(); } @@ -115,8 +139,7 @@ auto StateManager::FillGraph() -> void { // Sanity check. Both values need to be equal // for (const auto &[mass, state] : masses) { - // std::cout << "Masses: " << mass << ", States: " << states.at(state) - // << std::endl; + // std::println("Masses: {}, States: {}", mass, states.at(state)); // } } @@ -138,7 +161,9 @@ auto StateManager::UpdateGraph() -> void { FindTargetDistances(); } - visited_states.insert(current_state); + // Adds the element with 0 if it doesn't exist + visited_states[current_state]++; + total_moves++; if (history.size() > 0 && history.top() == current_state) { // We don't pop the stack when moving backwards to indicate if we need to @@ -166,7 +191,7 @@ auto StateManager::ClearGraph() -> void { // Re-add the default stuff to the graph states.insert(std::make_pair(current_state, states.size())); masses.insert(std::make_pair(states.size() - 1, current_state)); - visited_states.insert(current_state); + visited_states.insert(std::make_pair(current_state, 1)); physics.AddMassCmd(); // These states are no longer in the graph @@ -201,9 +226,8 @@ auto StateManager::FindTargetDistances() -> void { target_distances = CalculateDistances(states.size(), springs, targets); - // std::cout << "Calculated " << target_distances.distances.size() - // << " distances to " << targets.size() << " targets." << - // std::endl; + // std::println("Calculated {} distances to {} targets.", + // target_distances.distances.size(), targets.size()); } auto StateManager::FindTargetPath() -> void { @@ -212,8 +236,7 @@ auto StateManager::FindTargetPath() -> void { } winning_path = GetPath(target_distances, CurrentMassIndex()); - // std::cout << "Nearest target is " << winning_path.size() << " moves away." - // << std::endl; + // std::println("Nearest target is {} moves away.", winning_path.size()); } auto StateManager::FindWorstState() -> State {