workspaceify

This commit is contained in:
2026-05-30 20:20:09 -05:00
parent d19737c113
commit a0d21bde20
14 changed files with 128 additions and 64 deletions
Generated
+20 -4
View File
@@ -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"
+3 -12
View File
@@ -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"
+10
View File
@@ -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"] }
+31 -14
View File
@@ -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<H: Hasher>(&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.
+4
View File
@@ -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;
+15 -16
View File
@@ -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<FontHeader>,
/// Any `[[objects]]` entries. Optional; defaults to empty.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub objects: Vec<MapFileObjectEntry>,
pub(crate) objects: Vec<MapFileObjectEntry>,
/// Any `[[portals]]` entries. Optional; defaults to empty.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub portals: Vec<PortalDef>,
@@ -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<String>,
}
/// 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);
+16
View File
@@ -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"] }

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+1 -1
View File
@@ -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)]
@@ -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.
///
@@ -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();
+9 -9
View File
@@ -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<BitmapFont>,
@@ -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,
);
}
+8 -3
View File
@@ -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.