workspaceify
This commit is contained in:
Generated
+20
-4
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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 |
@@ -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();
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user