diff --git a/kiln-core/src/game.rs b/kiln-core/src/game.rs index 4debe5e..bb335dd 100644 --- a/kiln-core/src/game.rs +++ b/kiln-core/src/game.rs @@ -320,6 +320,8 @@ pub struct Board { pub portals: Vec, /// Optional font override for this board. When `None`, the app default is used. pub font: Option, + /// Integer scale factor applied to tiles when rendering this board. 1 = natural size. + pub zoom: u32, /// Named Rhai scripts available on this board: script name → source text. /// /// All script source lives here; [`ObjectDef`]s and [`Board::board_script_name`] diff --git a/kiln-core/src/map_file.rs b/kiln-core/src/map_file.rs index e957a16..c0233a7 100644 --- a/kiln-core/src/map_file.rs +++ b/kiln-core/src/map_file.rs @@ -50,6 +50,9 @@ pub struct MapHeader { /// Name of the board-level script in the `[scripts]` table, if any. #[serde(default, skip_serializing_if = "Option::is_none")] pub board_script_name: Option, + /// Integer tile scale factor. 1 = natural size. Omitted from files when 1 (the default). + #[serde(default = "default_zoom", skip_serializing_if = "is_zoom_default")] + pub zoom: u32, } /// The `[font]` section of a map file, specifying a bitmap font for this board. @@ -150,6 +153,8 @@ pub(crate) struct MapFileObjectEntry { } fn default_true() -> bool { true } +fn default_zoom() -> u32 { 1 } +fn is_zoom_default(z: &u32) -> bool { *z == 1 } /// Parses an `"#RRGGBB"` hex color string into an [`Rgba8`]. /// Returns opaque black on any parse failure. @@ -282,6 +287,7 @@ impl TryFrom for Board { objects, portals: mf.portals, font, + zoom: mf.map.zoom, scripts: mf.scripts, board_script_name: mf.map.board_script_name, }) @@ -399,6 +405,7 @@ impl From<&Board> for MapFile { height: board.height, player_start: [board.player.x, board.player.y], board_script_name: board.board_script_name.clone(), + zoom: board.zoom, }, palette, grid: GridData { content }, diff --git a/kiln-egui/src/editor.rs b/kiln-egui/src/editor.rs index 3659286..a9d4836 100644 --- a/kiln-egui/src/editor.rs +++ b/kiln-egui/src/editor.rs @@ -227,6 +227,10 @@ pub(crate) fn show_editor_panel( FontDialogState::from_spec(board.font.as_ref()); editor.font_dialog_open = true; } + + ui.separator(); + ui.label("Zoom"); + ui.add(egui::Slider::new(&mut board.zoom, 1..=8).text("×")); } EditorTab::Scripts => { diff --git a/kiln-egui/src/main.rs b/kiln-egui/src/main.rs index cdf372f..71b2f13 100644 --- a/kiln-egui/src/main.rs +++ b/kiln-egui/src/main.rs @@ -197,8 +197,9 @@ impl eframe::App for App { // Snapshot the active font for this frame so all rendering uses the same one. // We re-borrow active_font here since apply_font_spec may have changed board_font. let font = self.board_font.as_ref().unwrap_or(&self.default_font); - let tw = font.tile_w as f32; - let th = font.tile_h as f32; + let zoom = self.state.board.zoom; + let tw = font.tile_w as f32 * zoom as f32; + let th = font.tile_h as f32 * zoom as f32; // --- Board rendering --- // The board is drawn using the egui Painter API (direct 2D drawing). @@ -211,7 +212,7 @@ impl eframe::App for App { // Edit mode: ScrollArea lets the user scroll when the board overflows. egui::ScrollArea::new([true, true]).show(ui, |ui| { let (rect, response) = ui.allocate_exact_size(board_px, Sense::click()); - draw_board(ui.painter(), rect.min, &self.state.board, font); + draw_board(ui.painter(), rect.min, &self.state.board, font, zoom); // When the Objects tab is active, overlay the object highlights. if self.editor.tab == EditorTab::Objects { @@ -221,6 +222,7 @@ impl eframe::App for App { &self.state.board, font, self.editor.selected_object, + zoom, ); } @@ -264,8 +266,8 @@ impl eframe::App for App { // Play mode: player-centered viewport, no scroll bars. let available = ui.available_rect_before_wrap(); let player = self.state.board.player; - let origin = board_origin(available, board_w, board_h, player, font); - draw_board(ui.painter(), origin, &self.state.board, font); + let origin = board_origin(available, board_w, board_h, player, font, zoom); + draw_board(ui.painter(), origin, &self.state.board, font, zoom); } }); diff --git a/kiln-egui/src/render.rs b/kiln-egui/src/render.rs index 526b232..e5a4f06 100644 --- a/kiln-egui/src/render.rs +++ b/kiln-egui/src/render.rs @@ -65,7 +65,7 @@ pub(crate) fn paint_glyph(painter: &egui::Painter, rect: Rect, glyph: &Glyph, fo /// /// `origin` is the pixel position of cell `(0, 0)` — the top-left corner of /// the board within the panel. Cell positions are computed from `origin` using -/// the font's tile dimensions. +/// the font's tile dimensions multiplied by `zoom`. pub(crate) fn draw_glyph( painter: &egui::Painter, origin: Pos2, @@ -73,9 +73,10 @@ pub(crate) fn draw_glyph( y: usize, glyph: &Glyph, font: &BitmapFont, + zoom: u32, ) { - let tw = font.tile_w as f32; - let th = font.tile_h as f32; + let tw = font.tile_w as f32 * zoom as f32; + let th = font.tile_h as f32 * zoom as f32; let tl = origin + vec2(x as f32 * tw, y as f32 * th); let rect = Rect::from_min_size(tl, vec2(tw, th)); paint_glyph(painter, rect, glyph, font); @@ -85,17 +86,24 @@ pub(crate) fn draw_glyph( /// /// Rendering order: grid (floor) → objects → player. Objects are drawn on top /// of their grid cell's background, which is revealed if the object moves away. -pub(crate) fn draw_board(painter: &egui::Painter, origin: Pos2, board: &Board, font: &BitmapFont) { +/// `zoom` scales each tile by an integer factor. +pub(crate) fn draw_board( + painter: &egui::Painter, + origin: Pos2, + board: &Board, + font: &BitmapFont, + zoom: u32, +) { // Pass 1: grid floor. 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, font); + draw_glyph(painter, origin, x, y, glyph, font, zoom); } } // Pass 2: objects drawn on top of the floor. for obj in &board.objects { - draw_glyph(painter, origin, obj.x, obj.y, &obj.glyph, font); + draw_glyph(painter, origin, obj.x, obj.y, &obj.glyph, font, zoom); } // Pass 3: player overlay. Will be removed once the player becomes a scripted object. let player_glyph = Glyph::player(); @@ -106,6 +114,7 @@ pub(crate) fn draw_board(painter: &egui::Painter, origin: Pos2, board: &Board, f board.player.y as usize, &player_glyph, font, + zoom, ); } @@ -114,16 +123,18 @@ pub(crate) fn draw_board(painter: &egui::Painter, origin: Pos2, board: &Board, f /// Dims the entire board with a semi-transparent fill, then for each /// [`ObjectDef`](crate::game::ObjectDef) redraws that cell at full brightness /// and outlines it in white. The selected object (by index into -/// `board.objects`) gets a yellow outline instead. +/// `board.objects`) gets a yellow outline instead. `zoom` must match the zoom +/// used to draw the board. pub(crate) fn draw_object_overlays( painter: &egui::Painter, origin: Pos2, board: &Board, font: &BitmapFont, selected: Option, + zoom: u32, ) { - let tw = font.tile_w as f32; - let th = font.tile_h as f32; + let tw = font.tile_w as f32 * zoom as f32; + let th = font.tile_h as f32 * zoom as f32; // Dim the whole board so non-object cells recede visually. let board_rect = Rect::from_min_size( @@ -166,16 +177,17 @@ pub(crate) fn pos_to_cell(origin: Pos2, pos: Pos2, tile_w: f32, tile_h: f32) -> /// /// If the board fits along an axis, it is centered. If it overflows, the /// viewport is centered on the player and clamped so no empty space appears -/// at the board edges. +/// at the board edges. `zoom` scales tile dimensions before computing pixel sizes. pub(crate) fn board_origin( available: Rect, board_w: usize, board_h: usize, player: Player, font: &BitmapFont, + zoom: u32, ) -> Pos2 { - let tw = font.tile_w as f32; - let th = font.tile_h as f32; + let tw = font.tile_w as f32 * zoom as f32; + let th = font.tile_h as f32 * zoom as f32; let board_px_w = board_w as f32 * tw; let board_px_h = board_h as f32 * th;