#import "Basic"; #import "Math"; #import "String"; #import "Random"; Maze_Data :: struct { rows: int; cols: int; grid: [..] bool; // false = empty, true = wall // Camera camera_x: float; camera_y: float; zoom: float; // Panning State is_panning: bool; last_mouse_x: float; last_mouse_y: float; // UI State width_input: [..] u8; height_input: [..] u8; font: *Simp.Dynamic_Font; panel: UI_Panel; // Generator State is_generating: bool; frontier: [..] Int2; time_since_last_step: float; generate_speed: float; } Int2 :: struct { x: int; // col y: int; // row } init_maze_app :: (widget: *Widget_State) { data := New(Maze_Data); data.rows = 10; data.cols = 10; array_resize(*data.grid, data.rows * data.cols); for 0..data.grid.count-1 { data.grid[it] = false; } data.camera_x = cast(float) window_width / 2.0 - (5.0 * 50.0); data.camera_y = cast(float) window_height / 2.0 - (5.0 * 50.0); data.zoom = 50.0; // 50 pixels per cell data.is_panning = false; // prefill text inputs // TODO reconsider if the UI element should just manage this directly and parse the given value into backing string buffer str_w := sprint("%", data.cols); defer free(str_w); for 0..str_w.count-1 array_add(*data.width_input, str_w[it]); str_h := sprint("%", data.rows); defer free(str_h); for 0..str_h.count-1 array_add(*data.height_input, str_h[it]); // Setup UI Panel data.panel.x = 20.0; data.panel.y = cast(float)window_height - 340.0; data.panel.w = 200.0; data.panel.h = 280.0; data.panel.userdata = data; // Give callback access to our data! data.panel.app_owner = widget; // Bind panel visibility to this app! array_add(*data.panel.elements, .{type=.HEADER, id=5, x0=0.0, y0=data.panel.h - 40.0, x1=data.panel.w, y1=data.panel.h, label="Controls"}); // Inputs (from top to bottom, note Simp Y=0 is bottom, so highest Y is top) array_add(*data.panel.elements, .{type=.TEXT_INPUT, id=1, x0=10.0, y0=data.panel.h - 100.0, x1=190.0, y1=data.panel.h - 60.0, label="Width:", input_buf=*data.width_input, text_filter=ui_filter_for_nums }); array_add(*data.panel.elements, .{type=.TEXT_INPUT, id=2, x0=10.0, y0=data.panel.h - 160.0, x1=190.0, y1=data.panel.h - 120.0, label="Height:", input_buf=*data.height_input,text_filter=ui_filter_for_nums }); // Generate Button array_add(*data.panel.elements, .{type=.BUTTON, id=3, x0=10.0, y0=data.panel.h - 220.0, x1=190.0, y1=data.panel.h - 180.0, label="Generate", action=generate_maze_callback}); // Back button at bottom array_add(*data.panel.elements, .{type=.BUTTON, id=4, x0=10.0, y0=10.0, x1=190.0, y1=50.0, label="Back", action=(userdata: *void) { switch_app(*menu_state); }}); widget.data = data; widget.init = (widget: *Widget_State) { data := cast(*Maze_Data) widget.data; data.panel.font = Simp.get_font_at_size("data", "Anonymous_Pro.ttf", 32); data.font = data.panel.font; ui_register_panel(*data.panel); }; widget.update = (widget: *Widget_State, dt: float) { data := cast(*Maze_Data) widget.data; if data.is_generating { data.time_since_last_step += dt; steps := cast(int)(data.time_since_last_step * data.generate_speed); if steps > 0 { data.time_since_last_step -= cast(float)steps / data.generate_speed; for 1..steps { if data.frontier.count > 0 { idx := cast(int)(random_get() % cast(u64)data.frontier.count); cell := data.frontier[idx]; array_ordered_remove_by_index(*data.frontier, idx); if !data.grid[cell.y * data.cols + cell.x] continue; // already visited empty_neighbors: [..] Int2; if cell.y >= 2 && !data.grid[(cell.y - 2) * data.cols + cell.x] array_add(*empty_neighbors, .{cell.x, cell.y - 2}); if cell.y < data.rows - 2 && !data.grid[(cell.y + 2) * data.cols + cell.x] array_add(*empty_neighbors, .{cell.x, cell.y + 2}); if cell.x >= 2 && !data.grid[cell.y * data.cols + cell.x - 2] array_add(*empty_neighbors, .{cell.x - 2, cell.y}); if cell.x < data.cols - 2 && !data.grid[cell.y * data.cols + cell.x + 2] array_add(*empty_neighbors, .{cell.x + 2, cell.y}); if empty_neighbors.count > 0 { n_idx := cast(int)(random_get() % cast(u64)empty_neighbors.count); n := empty_neighbors[n_idx]; wall_x := (cell.x + n.x) / 2; wall_y := (cell.y + n.y) / 2; data.grid[wall_y * data.cols + wall_x] = false; data.grid[cell.y * data.cols + cell.x] = false; if cell.y >= 3 && data.grid[(cell.y - 2) * data.cols + cell.x] array_add(*data.frontier, .{cell.x, cell.y - 2}); if cell.y < data.rows - 3 && data.grid[(cell.y + 2) * data.cols + cell.x] array_add(*data.frontier, .{cell.x, cell.y + 2}); if cell.x >= 3 && data.grid[cell.y * data.cols + cell.x - 2] array_add(*data.frontier, .{cell.x - 2, cell.y}); if cell.x < data.cols - 3 && data.grid[cell.y * data.cols + cell.x + 2] array_add(*data.frontier, .{cell.x + 2, cell.y}); } array_free(empty_neighbors); } else { data.is_generating = false; break; } } } } }; widget.handle_input = handle_input; widget.render = render; widget.shutdown = (widget: *Widget_State) { data := cast(*Maze_Data) widget.data; ui_unregister_panel(*data.panel); array_free(data.grid); array_free(data.width_input); array_free(data.height_input); array_free(data.frontier); free(data); }; } #scope_file generate_maze_callback :: (userdata: *void) { data := cast(*Maze_Data) userdata; // Fill with walls for r: 0..data.rows-1 { for c: 0..data.cols-1 { data.grid[r * data.cols + c] = true; } } // Pick start node (odd, odd) start_r := 1; start_c := 1; if data.rows > 2 && data.cols > 2 { start_r = cast(int)(random_get() % cast(u64)(data.rows / 2)) * 2 + 1; start_c = cast(int)(random_get() % cast(u64)(data.cols / 2)) * 2 + 1; } data.grid[start_r * data.cols + start_c] = false; array_reset(*data.frontier); // Add neighbors of start node if start_r >= 3 array_add(*data.frontier, .{start_c, start_r - 2}); if start_r < data.rows - 3 array_add(*data.frontier, .{start_c, start_r + 2}); if start_c >= 3 array_add(*data.frontier, .{start_c - 2, start_r}); if start_c < data.cols - 3 array_add(*data.frontier, .{start_c + 2, start_r}); data.is_generating = true; data.time_since_last_step = 0; data.generate_speed = 60.0; // steps per second } apply_resize :: (data: *Maze_Data) { data.is_generating = false; w_str: string; w_str.data = data.width_input.data; w_str.count = data.width_input.count; h_str: string; h_str.data = data.height_input.data; h_str.count = data.height_input.count; new_cols, ok_w := string_to_int(w_str); new_rows, ok_h := string_to_int(h_str); if ok_w && ok_h && new_cols > 0 && new_rows > 0 { new_grid: [..] bool; array_resize(*new_grid, new_rows * new_cols); for r: 0..new_rows-1 { for c: 0..new_cols-1 { val := false; if r < data.rows && c < data.cols { val = data.grid[r * data.cols + c]; } new_grid[r * new_cols + c] = val; } } array_free(data.grid); data.grid = new_grid; data.rows = new_rows; data.cols = new_cols; } } handle_input :: (widget: *Widget_State, occluded: bool) -> (occludes: bool) { data := cast(*Maze_Data) widget.data; mouse_x, mouse_y, success := get_mouse_pointer_position(window, true); if !success return occluded; mouse_x_f := cast(float) mouse_x; mouse_y_f := cast(float) mouse_y; space_state := Input.input_button_states[Input.Key_Code.SPACEBAR]; space_down := (space_state & Input.Key_Current_State.DOWN) != 0; for event: Input.events_this_frame { if event.type == .MOUSE_WHEEL { zoom_factor := 1.0; if event.wheel_delta < 0 { zoom_factor = 0.9; } else if event.wheel_delta > 0 { zoom_factor = 1.1; } if zoom_factor != 1.0 { world_x := (mouse_x_f - data.camera_x) / data.zoom; world_y := (mouse_y_f - data.camera_y) / data.zoom; data.zoom *= zoom_factor; if data.zoom < 5.0 data.zoom = 5.0; if data.zoom > 300.0 data.zoom = 300.0; data.camera_x = mouse_x_f - world_x * data.zoom; data.camera_y = mouse_y_f - world_y * data.zoom; } } // if event.type == .TEXT_INPUT && data.panel.focus != 0 { // print("we are text inputting into panel %", data.panel.focus); // value := event.utf32; // if value >= #char "0" && value <= #char "9" { // if data.panel.focus == 1 array_add(*data.width_input, cast(u8)value); // if data.panel.focus == 2 array_add(*data.height_input, cast(u8)value); // } // } if event.type == .KEYBOARD && event.key_pressed { if event.key_code == .ENTER { if data.panel.focus != 0 { apply_resize(data); data.panel.focus = 0; ui_clear_focus(); } } // Mouse clicks if event.key_code == .MOUSE_BUTTON_LEFT { if !occluded { // data.panel.focus = 0; // Clicked on grid (only if not panning with spacebar) if !space_down { world_x := (mouse_x_f - data.camera_x) / data.zoom; world_y := (mouse_y_f - data.camera_y) / data.zoom; col := cast(int) world_x; row := cast(int) world_y; // Check if within bounds if col >= 0 && col < data.cols && row >= 0 && row < data.rows { data.grid[row * data.cols + col] = !data.grid[row * data.cols + col]; } } } } } } mouse_down := (Input.input_button_states[Input.Key_Code.MOUSE_BUTTON_LEFT] & Input.Key_Current_State.DOWN) != 0; // Panning logic if space_down && mouse_down { if !data.is_panning { data.is_panning = true; data.last_mouse_x = mouse_x_f; data.last_mouse_y = mouse_y_f; } else { dx := mouse_x_f - data.last_mouse_x; dy := mouse_y_f - data.last_mouse_y; data.camera_x += dx; data.camera_y += dy; data.last_mouse_x = mouse_x_f; data.last_mouse_y = mouse_y_f; } } else { data.is_panning = false; } return false; } render :: (widget: *Widget_State) { data := cast(*Maze_Data) widget.data; Simp.clear_render_target(0.1, 0.1, 0.1, 1.0); Simp.set_shader_for_color(true); Simp.immediate_begin(); Simp.immediate_set_2d_projection(window_width, window_height); // Draw cells for r: 0..data.rows-1 { for c: 0..data.cols-1 { x0 := data.camera_x + cast(float) c * data.zoom; y0 := data.camera_y + cast(float) r * data.zoom; cx0 := x0 + 1.0; cy0 := y0 + 1.0; cx1 := x0 + data.zoom - 1.0; cy1 := y0 + data.zoom - 1.0; // Culling optimization if cx1 < 0 || cx0 > cast(float)window_width || cy1 < 0 || cy0 > cast(float)window_height continue; is_wall := data.grid[r * data.cols + c]; color := ifx is_wall then Vector4.{0.8, 0.2, 0.2, 1.0} else Vector4.{0.6, 0.6, 0.6, 1.0}; Simp.immediate_quad(cx0, cy0, cx1, cy1, color); } } // UI_Panel drawing is entirely handled by the global Window Manager in main.jai! }