diff --git a/Cargo.lock b/Cargo.lock index 4bc8f13..3b1eebc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -658,6 +658,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" + [[package]] name = "combine" version = "4.6.7" @@ -1705,17 +1711,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] -name = "kiln" +name = "kiln-core" version = "0.1.0" dependencies = [ - "eframe", - "image", - "rfd", + "color", "rhai", "serde", "toml", ] +[[package]] +name = "kiln-egui" +version = "0.1.0" +dependencies = [ + "color", + "eframe", + "image", + "kiln-core", + "rfd", + "serde", +] + [[package]] name = "leb128fmt" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 4d0a15b..977628e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,3 @@ -[package] -name = "kiln" -version = "0.1.0" -edition = "2024" - -[dependencies] -eframe = "0.33.3" -rhai = "1" -serde = { version = "1", features = ["derive"] } -toml = { version = "0.8", features = ["preserve_order"] } -image = { version = "0.25", default-features = false, features = ["png"] } -rfd = "0.15" +[workspace] +members = ["kiln-core", "kiln-egui"] +resolver = "2" diff --git a/kiln-core/Cargo.toml b/kiln-core/Cargo.toml new file mode 100644 index 0000000..7ce24b1 --- /dev/null +++ b/kiln-core/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "kiln-core" +version = "0.1.0" +edition = "2024" + +[dependencies] +color = "0.3.3" +rhai = "1" +serde = { version = "1", features = ["derive"] } +toml = { version = "0.8", features = ["preserve_order"] } diff --git a/src/game.rs b/kiln-core/src/game.rs similarity index 92% rename from src/game.rs rename to kiln-core/src/game.rs index 4a8e5d6..ef9374a 100644 --- a/src/game.rs +++ b/kiln-core/src/game.rs @@ -1,6 +1,7 @@ -use eframe::egui::Color32; +use color::Rgba8; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::hash::{Hash, Hasher}; /// The visual representation of a single board cell. /// @@ -14,14 +15,23 @@ use std::collections::HashMap; /// `Glyph` values come from the map file palette and are set at load time. /// The player is the only entity whose glyph is hardcoded at runtime /// (see [`Glyph::player`]). -#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, PartialEq, Eq)] pub struct Glyph { /// Tile index into the board's bitmap font (left-to-right, top-to-bottom). pub tile: u32, /// Foreground color, applied to non-background pixels of the tile. - pub fg: Color32, + pub fg: Rgba8, /// Background color, drawn as a filled rectangle behind the tile. - pub bg: Color32, + pub bg: Rgba8, +} + +impl Hash for Glyph { + /// Hash via packed u32 representations so the impl stays in sync with Eq. + fn hash(&self, state: &mut H) { + self.tile.hash(state); + self.fg.to_u32().hash(state); + self.bg.to_u32().hash(state); + } } impl Glyph { @@ -33,8 +43,8 @@ impl Glyph { pub const fn player() -> Self { Self { tile: 64, - fg: Color32::LIGHT_BLUE, - bg: Color32::BLACK, + fg: Rgba8 { r: 173, g: 216, b: 230, a: 255 }, // light blue + bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 }, // black } } } @@ -144,24 +154,24 @@ impl Archetype { match self { Archetype::Empty => Glyph { tile: 32, - fg: Color32::BLACK, - bg: Color32::BLACK, + fg: Rgba8 { r: 0, g: 0, b: 0, a: 255 }, + bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 }, }, Archetype::Wall => Glyph { tile: 35, - fg: Color32::from_rgb(0x80, 0x80, 0x80), - bg: Color32::from_rgb(0x60, 0x60, 0x60), + fg: Rgba8 { r: 0x80, g: 0x80, b: 0x80, a: 255 }, + bg: Rgba8 { r: 0x60, g: 0x60, b: 0x60, a: 255 }, }, Archetype::Object => Glyph { tile: 63, - fg: Color32::YELLOW, - bg: Color32::BLACK, + fg: Rgba8 { r: 255, g: 255, b: 0, a: 255 }, // yellow + bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 }, }, // Visually distinct so malformed map files are immediately obvious. Archetype::ErrorBlock => Glyph { tile: 63, - fg: Color32::YELLOW, - bg: Color32::RED, + fg: Rgba8 { r: 255, g: 255, b: 0, a: 255 }, // yellow on red + bg: Rgba8 { r: 255, g: 0, b: 0, a: 255 }, }, } } @@ -323,6 +333,13 @@ impl Board { &mut self.cells[y * self.width + x] } + /// Returns a slice of all cells in row-major order. + /// + /// Useful for iterating over the full board (e.g. to collect unique glyphs). + pub fn cells(&self) -> &[(Glyph, Archetype)] { + &self.cells + } + /// Returns `true` if the cell at `(x, y)` allows entities to pass through. /// /// Objects block movement before the grid cell archetype is consulted. diff --git a/kiln-core/src/lib.rs b/kiln-core/src/lib.rs new file mode 100644 index 0000000..c3e3eeb --- /dev/null +++ b/kiln-core/src/lib.rs @@ -0,0 +1,4 @@ +/// Core game types: [`game::Board`], [`game::Glyph`], [`game::Archetype`], etc. +pub mod game; +/// Map file loading and saving (`.toml` format). +pub mod map_file; diff --git a/src/map_file.rs b/kiln-core/src/map_file.rs similarity index 96% rename from src/map_file.rs rename to kiln-core/src/map_file.rs index 39c320c..c1096ce 100644 --- a/src/map_file.rs +++ b/kiln-core/src/map_file.rs @@ -1,5 +1,5 @@ use crate::game::{Archetype, Board, FontSpec, Glyph, ObjectDef, Player, PortalDef}; -use eframe::egui::Color32; +use color::Rgba8; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::convert::TryFrom; @@ -26,7 +26,7 @@ pub struct MapFile { pub font: Option, /// Any `[[objects]]` entries. Optional; defaults to empty. #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub objects: Vec, + pub(crate) objects: Vec, /// Any `[[portals]]` entries. Optional; defaults to empty. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub portals: Vec, @@ -73,7 +73,7 @@ pub struct FontHeader { /// the key to `tile`; the char form keeps working. #[derive(Deserialize, Serialize)] #[serde(untagged)] -enum TileIndex { +pub(crate) enum TileIndex { /// A direct tile index (e.g. `tile = 35`). Num(u32), /// A single-character shorthand (e.g. `tile = "#"`); converted to its Unicode scalar. @@ -81,7 +81,7 @@ enum TileIndex { } impl TileIndex { - fn into_u32(self) -> u32 { + pub(crate) fn into_u32(self) -> u32 { match self { TileIndex::Num(n) => n, TileIndex::Chr(c) => c as u32, @@ -143,22 +143,22 @@ pub(crate) struct MapFileObjectEntry { script_name: Option, } -/// Parses an `"#RRGGBB"` hex color string into a [`Color32`]. -/// Returns black on any parse failure. -fn parse_color(hex: &str) -> Color32 { +/// Parses an `"#RRGGBB"` hex color string into an [`Rgba8`]. +/// Returns opaque black on any parse failure. +fn parse_color(hex: &str) -> Rgba8 { let hex = hex.trim_start_matches('#'); if hex.len() != 6 { - return Color32::BLACK; + return Rgba8 { r: 0, g: 0, b: 0, a: 255 }; } let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0); let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0); let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0); - Color32::from_rgb(r, g, b) + Rgba8 { r, g, b, a: 255 } } -/// Converts a [`Color32`] to an `"#RRGGBB"` hex string. -fn color_to_hex(color: Color32) -> String { - format!("#{:02X}{:02X}{:02X}", color.r(), color.g(), color.b()) +/// Converts an [`Rgba8`] to an `"#RRGGBB"` hex string (alpha is ignored). +fn color_to_hex(color: Rgba8) -> String { + format!("#{:02X}{:02X}{:02X}", color.r, color.g, color.b) } /// Converts a parsed map file into a runtime [`Board`]. @@ -368,6 +368,7 @@ impl From<&Board> for MapFile { script_name: o.script_name.clone(), }) .collect(); + let portals = board .portals .iter() @@ -498,8 +499,6 @@ O #[test] fn object_glyph_round_trips_through_toml() { // Objects must survive a save→load cycle with their glyph intact. - use eframe::egui::Color32; - let toml = r##" [map] name = "Test" @@ -531,8 +530,8 @@ bg = "#000000" assert_eq!(obj.x, 1); assert_eq!(obj.y, 1); assert_eq!(obj.glyph.tile, 64); - assert_eq!(obj.glyph.fg, Color32::from_rgb(0x00, 0xFF, 0xFF)); - assert_eq!(obj.glyph.bg, Color32::BLACK); + assert_eq!(obj.glyph.fg, Rgba8 { r: 0x00, g: 0xFF, b: 0xFF, a: 255 }); + assert_eq!(obj.glyph.bg, Rgba8 { r: 0, g: 0, b: 0, a: 255 }); // Save back to TOML and reload; glyph must survive. let map_file = MapFile::from(&board); diff --git a/kiln-egui/Cargo.toml b/kiln-egui/Cargo.toml new file mode 100644 index 0000000..5f2c05a --- /dev/null +++ b/kiln-egui/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "kiln-egui" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "kiln" +path = "src/main.rs" + +[dependencies] +kiln-core = { path = "../kiln-core" } +eframe = "0.33.3" +color = "0.3.3" +image = { version = "0.25", default-features = false, features = ["png"] } +rfd = "0.15" +serde = { version = "1", features = ["derive"] } diff --git a/assets/vga-font-8x16.png b/kiln-egui/assets/vga-font-8x16.png similarity index 100% rename from assets/vga-font-8x16.png rename to kiln-egui/assets/vga-font-8x16.png diff --git a/src/editor.rs b/kiln-egui/src/editor.rs similarity index 99% rename from src/editor.rs rename to kiln-egui/src/editor.rs index 75642d5..7c95050 100644 --- a/src/editor.rs +++ b/kiln-egui/src/editor.rs @@ -3,8 +3,8 @@ use egui::{Sense, vec2}; use crate::font::BitmapFont; use crate::font_dialog::{FontDialogState, show as show_font_dialog}; -use crate::game::{ALL_ARCHETYPES, Archetype, Board, Glyph}; use crate::render::{PALETTE_PANEL_W, paint_glyph}; +use kiln_core::game::{ALL_ARCHETYPES, Archetype, Board, Glyph}; /// Which tab is active in the editor side panel. #[derive(Copy, Clone, PartialEq, Eq)] diff --git a/src/font.rs b/kiln-egui/src/font.rs similarity index 100% rename from src/font.rs rename to kiln-egui/src/font.rs diff --git a/src/font_dialog.rs b/kiln-egui/src/font_dialog.rs similarity index 99% rename from src/font_dialog.rs rename to kiln-egui/src/font_dialog.rs index 6fd17ea..be335b5 100644 --- a/src/font_dialog.rs +++ b/kiln-egui/src/font_dialog.rs @@ -2,7 +2,7 @@ use eframe::egui; use egui::{Color32, DragValue, Rect, Stroke, vec2}; use crate::font::BitmapFont; -use crate::game::FontSpec; +use kiln_core::game::FontSpec; /// State for the font-picker dialog. /// diff --git a/src/glyph_picker.rs b/kiln-egui/src/glyph_picker.rs similarity index 89% rename from src/glyph_picker.rs rename to kiln-egui/src/glyph_picker.rs index ed33ad2..4d622e9 100644 --- a/src/glyph_picker.rs +++ b/kiln-egui/src/glyph_picker.rs @@ -4,8 +4,8 @@ use eframe::egui; use egui::{Color32, Rect, Sense, Stroke, vec2}; use crate::font::BitmapFont; -use crate::game::{Archetype, Glyph}; -use crate::render::{paint_glyph, pos_to_cell}; +use crate::render::{paint_glyph, pos_to_cell, rgba8_to_color32}; +use kiln_core::game::{Archetype, Glyph}; /// Renders the floating glyph-picker dialog and updates `glyph` in place. /// @@ -62,18 +62,24 @@ pub(crate) fn show( ui.label("Colors"); ui.horizontal(|ui| { + // The color picker works with Color32; convert to/from Rgba8 around the call. + let mut fg = rgba8_to_color32(glyph.fg); ui.label("FG"); egui::color_picker::color_edit_button_srgba( ui, - &mut glyph.fg, + &mut fg, egui::color_picker::Alpha::Opaque, ); + glyph.fg = color::Rgba8 { r: fg.r(), g: fg.g(), b: fg.b(), a: fg.a() }; + + let mut bg = rgba8_to_color32(glyph.bg); ui.label("BG"); egui::color_picker::color_edit_button_srgba( ui, - &mut glyph.bg, + &mut bg, egui::color_picker::Alpha::Opaque, ); + glyph.bg = color::Rgba8 { r: bg.r(), g: bg.g(), b: bg.b(), a: bg.a() }; }); ui.separator(); diff --git a/src/main.rs b/kiln-egui/src/main.rs similarity index 95% rename from src/main.rs rename to kiln-egui/src/main.rs index bad475f..6b3a21d 100644 --- a/src/main.rs +++ b/kiln-egui/src/main.rs @@ -5,14 +5,13 @@ use std::path::{Path, PathBuf}; mod editor; mod font; mod font_dialog; -mod game; mod glyph_picker; -mod map_file; mod render; use editor::{EditorState, EditorTab, show_editor_panel}; use font::BitmapFont; -use game::{Archetype, FontSpec, GameState, Glyph, ObjectDef}; +use kiln_core::game::{Archetype, FontSpec, GameState, Glyph, ObjectDef}; +use kiln_core::map_file; use render::{ DEFAULT_WINDOW_H, DEFAULT_WINDOW_W, MIN_WINDOW_H, MIN_WINDOW_W, board_origin, draw_board, draw_object_overlays, pos_to_cell, @@ -56,7 +55,7 @@ struct App { state: GameState, mode: AppMode, editor: EditorState, - /// The default font, loaded from `assets/default_font.png` or a placeholder. + /// The default font, embedded in the binary and loaded at startup. default_font: BitmapFont, /// A per-board font loaded from `board.font`, if one is specified. board_font: Option, @@ -71,9 +70,10 @@ impl App { /// `egui_ctx` is needed to upload font textures before the first frame. /// `initial_path` is the file the board was loaded from and becomes the /// default Save target. - fn new(board: game::Board, egui_ctx: &egui::Context, initial_path: PathBuf) -> Self { - // Load the default font from assets/. Fall back to a placeholder if missing. - let default_font = BitmapFont::load(egui_ctx, Path::new("assets/vga-font-8x16.png"), 8, 16) + fn new(board: kiln_core::game::Board, egui_ctx: &egui::Context, initial_path: PathBuf) -> Self { + // Default font is embedded in the binary; no runtime file I/O needed. + let font_bytes = include_bytes!("../assets/vga-font-8x16.png"); + let default_font = BitmapFont::from_bytes(egui_ctx, font_bytes, 8, 16, "vga-font-8x16") .unwrap_or_else(|_| BitmapFont::create_placeholder(egui_ctx)); // If the board specifies its own font, try to load it now. @@ -286,7 +286,7 @@ impl eframe::App for App { ctx, &mut self.editor.glyph_picker_open, &mut self.editor.glyph, - &self.state.board.cells, + &self.state.board.cells(), font, ); } @@ -299,7 +299,7 @@ impl eframe::App for App { ctx, &mut self.editor.object_glyph_picker_open, &mut self.editor.object_editing_glyph, - &self.state.board.cells, + &self.state.board.cells(), font, ); } diff --git a/src/render.rs b/kiln-egui/src/render.rs similarity index 95% rename from src/render.rs rename to kiln-egui/src/render.rs index 4c0ab6b..526b232 100644 --- a/src/render.rs +++ b/kiln-egui/src/render.rs @@ -28,7 +28,12 @@ use eframe::egui; use egui::{Color32, Pos2, Rect, vec2}; use crate::font::BitmapFont; -use crate::game::{Board, Glyph, Player}; +use kiln_core::game::{Board, Glyph, Player}; + +/// Converts a core [`Glyph`] color ([`color::Rgba8`]) to egui [`Color32`]. +pub(crate) fn rgba8_to_color32(c: color::Rgba8) -> Color32 { + Color32::from_rgba_unmultiplied(c.r, c.g, c.b, c.a) +} /// Approximate height of the egui menu bar in pixels, used for window sizing. pub(crate) const MENU_H: f32 = 24.0; @@ -51,9 +56,9 @@ pub(crate) const MIN_WINDOW_H: f32 = 8.0 * 5.0 + MENU_H + 2.0 * PANEL_MARGIN; /// `glyph.fg`. Unlike [`draw_glyph`], the caller owns the rect and handles /// interaction — use this when you need the click [`egui::Response`]. pub(crate) fn paint_glyph(painter: &egui::Painter, rect: Rect, glyph: &Glyph, font: &BitmapFont) { - painter.rect_filled(rect, 0.0, glyph.bg); + painter.rect_filled(rect, 0.0, rgba8_to_color32(glyph.bg)); let uv = font.tile_uv(glyph.tile); - painter.image(font.texture.id(), rect, uv, glyph.fg); + painter.image(font.texture.id(), rect, uv, rgba8_to_color32(glyph.fg)); } /// Draws a single cell at grid position `(x, y)` using the given glyph and font.