diff --git a/src/main.rs b/src/main.rs index 2fa473d..9f9918d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ use eframe::egui; -use egui::{Align2, FontId, Key, Pos2, Rect, vec2}; +use egui::{Align2, FontId, Key, Pos2, Rect, Sense, vec2}; mod game; mod map_file; -use game::{GameState, Glyph}; +use game::{Archetype, GameState, Glyph, ALL_ARCHETYPES}; /// Width of one board cell in pixels. const CELL_W: f32 = 14.0; @@ -15,6 +15,33 @@ const MENU_H: f32 = 24.0; /// Default inner margin that egui's CentralPanel adds on all sides, in pixels. /// The minimum window size accounts for this so the board fits without clipping. const PANEL_MARGIN: f32 = 8.0; +/// Width of the editor palette side panel in pixels. +const PALETTE_PANEL_W: f32 = 120.0; + +/// Whether the application is in play mode or edit mode. +/// +/// In `Play` mode the game runs normally: arrow keys move the player. +/// In `Edit` mode arrow keys are inactive, a side panel shows the archetype +/// palette, and clicking on the viewport paints cells. +#[derive(Copy, Clone, PartialEq, Eq)] +enum AppMode { + /// Normal game play. Arrow keys control the player. + Play, + /// Board editing. Side panel shows archetypes; click to paint cells. + Edit, +} + +/// Transient editor state. Only meaningful when [`AppMode::Edit`] is active. +struct EditorState { + /// The archetype that will be stamped when the user clicks a cell. + selected: Archetype, +} + +impl EditorState { + fn new() -> Self { + Self { selected: Archetype::Wall } + } +} fn main() -> eframe::Result<()> { // Load the board before creating the window so its dimensions can drive @@ -23,6 +50,8 @@ fn main() -> eframe::Result<()> { let board_px_w = board.width as f32 * CELL_W + 2.0 * PANEL_MARGIN; let board_px_h = board.height as f32 * CELL_H + MENU_H + 2.0 * PANEL_MARGIN; + // Window min-size is sized for Play mode. In Edit mode the palette panel + // steals PALETTE_PANEL_W pixels; the board may clip at minimum window size. let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([board_px_w, board_px_h]) @@ -42,25 +71,32 @@ fn main() -> eframe::Result<()> { /// updates, and rendering happen there. There is no separate game loop thread. struct App { state: GameState, + mode: AppMode, + editor: EditorState, } impl App { fn new(board: game::Board) -> Self { - Self { state: GameState::new(board) } + Self { + state: GameState::new(board), + mode: AppMode::Play, + editor: EditorState::new(), + } } } impl eframe::App for App { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { // --- Input --- - // Arrow keys are read before any panel is drawn. egui accumulates - // key events between frames; key_pressed returns true once per press. - ctx.input(|i| { - if i.key_pressed(Key::ArrowUp) { self.state.try_move( 0, -1); } - if i.key_pressed(Key::ArrowDown) { self.state.try_move( 0, 1); } - if i.key_pressed(Key::ArrowLeft) { self.state.try_move(-1, 0); } - if i.key_pressed(Key::ArrowRight) { self.state.try_move( 1, 0); } - }); + // Arrow keys move the player only in Play mode. + if self.mode == AppMode::Play { + ctx.input(|i| { + if i.key_pressed(Key::ArrowUp) { self.state.try_move( 0, -1); } + if i.key_pressed(Key::ArrowDown) { self.state.try_move( 0, 1); } + if i.key_pressed(Key::ArrowLeft) { self.state.try_move(-1, 0); } + if i.key_pressed(Key::ArrowRight) { self.state.try_move( 1, 0); } + }); + } // --- Menu bar --- egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { @@ -70,44 +106,98 @@ impl eframe::App for App { ctx.send_viewport_cmd(egui::ViewportCommand::Close); } }); + // Mode toggle: Play keeps the game running; Edit shows the palette panel. + ui.separator(); + ui.selectable_value(&mut self.mode, AppMode::Play, "Play"); + ui.selectable_value(&mut self.mode, AppMode::Edit, "Edit"); }); }); + // --- Editor palette panel --- + // Must be declared before CentralPanel so egui allocates its space first. + if self.mode == AppMode::Edit { + egui::SidePanel::right("palette_panel") + .resizable(false) + .exact_width(PALETTE_PANEL_W) + .show(ctx, |ui| { + ui.heading("Elements"); + ui.separator(); + for archetype in ALL_ARCHETYPES { + ui.selectable_value( + &mut self.editor.selected, + *archetype, + archetype.name(), + ); + } + }); + } + // --- Board rendering --- // The board is drawn using the egui Painter API (direct 2D drawing), // not egui widgets. This gives pixel-level control over placement. egui::CentralPanel::default().show(ctx, |ui| { - let board = &self.state.board; let available = ui.available_rect_before_wrap(); // Center the board within the panel. If the window is at minimum // size the offset is zero; if the window is larger the board floats // in the middle. - let board_px = vec2(board.width as f32 * CELL_W, board.height as f32 * CELL_H); + let board_px = vec2( + self.state.board.width as f32 * CELL_W, + self.state.board.height as f32 * CELL_H, + ); let offset = ((available.size() - board_px) / 2.0).max(egui::Vec2::ZERO); let origin = available.min + offset; - let painter = ui.painter(); + // Draw board and player. Immutable borrow of `board` ends after + // this block, freeing `self.state.board` for mutation below. + { + let board = &self.state.board; + let painter = ui.painter(); - // Draw each board cell: filled background rect, then the character. - for y in 0..board.height { - for x in 0..board.width { - let (glyph, _) = board.get(x, y); - draw_glyph(painter, origin, x, y, glyph); + // Draw each board cell: filled background rect, then the character. + for y in 0..board.height { + for x in 0..board.width { + let (glyph, _) = board.get(x, y); + draw_glyph(painter, origin, x, y, glyph); + } } + + // Draw the player on top of the board. The player is not stored as + // a board cell, so it is rendered as a separate overlay. This will + // change once the player becomes a scripted object. + let player_glyph = Glyph::player(); + draw_glyph( + painter, + origin, + board.player.x as usize, + board.player.y as usize, + &player_glyph, + ); } - // Draw the player on top of the board. The player is not stored as - // a board cell, so it is rendered as a separate overlay. This will - // change once the player becomes a scripted object. - let player_glyph = Glyph::player(); - draw_glyph( - painter, - origin, - self.state.board.player.x as usize, - self.state.board.player.y as usize, - &player_glyph, - ); + // In Edit mode, capture clicks on the viewport and paint cells. + if self.mode == AppMode::Edit { + let response = ui.allocate_rect(available, Sense::click()); + if response.clicked() { + if let Some(pos) = response.interact_pointer_pos() { + // Convert pixel position to cell coordinates using the + // same origin computed for rendering above. + let rel = pos - origin; + let cx = (rel.x / CELL_W).floor() as i32; + let cy = (rel.y / CELL_H).floor() as i32; + // floor() keeps out-of-bounds negatives negative; caught here. + if cx >= 0 && cy >= 0 { + let cx = cx as usize; + let cy = cy as usize; + let board = &mut self.state.board; + if cx < board.width && cy < board.height { + let arch = self.editor.selected; + *board.get_mut(cx, cy) = (arch.default_glyph(), arch); + } + } + } + } + } }); // egui repaints automatically on input events, so no explicit @@ -131,4 +221,4 @@ fn draw_glyph(painter: &egui::Painter, origin: Pos2, x: usize, y: usize, glyph: let center = rect.center(); painter.rect_filled(rect, 0.0, glyph.bg); painter.text(center, Align2::CENTER_CENTER, glyph.ch, FontId::monospace(CELL_H), glyph.fg); -} +} \ No newline at end of file