the huge refactor

This commit is contained in:
2026-07-24 21:52:05 -05:00
parent 5146cc9bcc
commit 917f4b1bf0
43 changed files with 1807 additions and 3267 deletions
+14 -15
View File
@@ -20,7 +20,7 @@ use kiln_core::game::GameState;
use kiln_core::glyph::Glyph;
use kiln_core::log::LogLine;
use kiln_core::world::World;
use kiln_core::{Archetype, Board, Builtin};
use kiln_core::{Board, Builtin};
use kiln_ui::code_editor::{CodeEditor, CodeEditorOutcome};
use kiln_ui::dialog::{Dialog, DialogResult, ListDialogResponse};
use ratatui::Frame;
@@ -31,6 +31,7 @@ use ratatui::symbols::merge::MergeStrategy;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph};
use std::cell::{Ref, RefMut};
use kiln_core::tile::TileSpec;
/// How long each half of the cursor blink lasts, in seconds.
const BLINK_SECS: f32 = 0.5;
@@ -70,7 +71,7 @@ pub(crate) struct EditorState {
/// The archetype the drawing tools stamp on [`place_current`](EditorState::place_current).
/// Defaults to [`Archetype::Wall`]; changed via the Terrain / Pushers menus
/// ([`set_current`](EditorState::set_current)).
current_archetype: Archetype,
current_archetype: &'static str,
/// The glyph the drawing tools stamp. Reset to the current archetype's default
/// whenever the archetype changes ([`set_current`](EditorState::set_current)), and
/// overridable via the glyph picker (`g`).
@@ -112,7 +113,7 @@ impl EditorState {
code_editor: None,
glyph_dialog: None,
cursor,
current_archetype: Archetype::Builtin(Builtin::Wall, "wall"),
current_archetype: "wall",
current_glyph: Builtin::Wall.default_glyph_for("wall"),
draw_mode: false,
sidebar_width: DEFAULT_SIDEBAR_WIDTH,
@@ -241,17 +242,21 @@ impl EditorState {
/// Sets the archetype the drawing tools stamp, resetting the drawing glyph to
/// that archetype's default (the glyph picker `g` can then override it). Called by
/// the Terrain / Pushers menu choices.
pub(crate) fn set_current(&mut self, archetype: Archetype) {
self.current_archetype = archetype;
self.current_glyph = self.current_archetype.default_glyph();
pub(crate) fn set_current(&mut self, archetype: &'static str) {
if let Some((builtin, variant)) = Builtin::from_name(archetype) {
self.current_archetype = variant;
self.current_glyph = builtin.default_glyph_for(variant);
}
}
/// Stamps the current drawing thing (archetype + glyph) into the cell under the
/// cursor, applying the placement rules (see [`Board::place_archetype`]).
pub(crate) fn place_current(&mut self) {
let (x, y) = (self.cursor.0 as usize, self.cursor.1 as usize);
let (arch, glyph) = (self.current_archetype, self.current_glyph);
self.board_mut().place_archetype(x, y, arch, glyph);
let res = self.board_mut().place(x, y, Some(TileSpec::Builtin { kind: self.current_archetype.to_string(), glyph: Some(self.current_glyph) }));
if let Err(e) = res {
self.log.push(LogLine::error(e))
}
}
/// Toggles draw mode (whether cursor movement auto-stamps the current thing).
@@ -330,12 +335,6 @@ impl EditorState {
let mut world = self.world.deep_clone();
// Start the playtest on the board open in the editor, not world.start.
world.start = self.board_name.clone();
// Editor-stamped machines (spinners, pushers) are plain terrain cells; expand
// them into their scripted objects so they actually run, just as the map loader
// does on disk-loaded worlds.
for board in world.boards.values() {
board.borrow_mut().expand_builtin_archetypes();
}
let mut game = GameState::from_world(world);
game.log(LogLine::raw("[esc] to return to the editor"));
game.run_init();
@@ -520,7 +519,7 @@ fn draw_footer_lines<'a>(
// Separator marking off the footer from the menu above.
Line::from(Span::styled("".repeat(64), sep_style)),
// The archetype currently being drawn (no UI to change it yet).
Line::from(Span::styled(ed.current_archetype.name(), label_style)),
Line::from(Span::styled(ed.current_archetype, label_style)),
Line::from(vec![
Span::styled("[G] ", key_style),
Span::styled("Glyph ", label_style),
+27 -27
View File
@@ -1,7 +1,7 @@
use crate::editor::EditorState;
use crate::menu::{MenuItem, MenuKey};
use crate::mode::PendingMode;
use kiln_core::{Archetype, Builtin};
use kiln_core::Builtin;
use kiln_core::keys::KeyType;
use kiln_core::glyph::Glyph;
@@ -77,10 +77,10 @@ impl MenuLevel {
MenuEntry::item('c', "Custom glyph [TODO]", |_| {}),
],
MenuLevel::Terrain => vec![
MenuEntry::item('w', "Wall", |ed| ed.set_current(Archetype::Builtin(Builtin::Wall, "wall"))),
MenuEntry::item('c', "■ Crate", |ed| ed.set_current(Archetype::Builtin(Builtin::Crate, "crate"))),
MenuEntry::item('v', "↕ Crate", |ed| ed.set_current(Archetype::Builtin(Builtin::VCrate, "vcrate"))),
MenuEntry::item('h', "↔ Crate", |ed| ed.set_current(Archetype::Builtin(Builtin::HCrate, "hcrate"))),
MenuEntry::item('w', "Wall", |ed| ed.set_current("wall")),
MenuEntry::item('c', "■ Crate", |ed| ed.set_current("crate")),
MenuEntry::item('v', "↕ Crate", |ed| ed.set_current("vcrate")),
MenuEntry::item('h', "↔ Crate", |ed| ed.set_current("hcrate")),
],
MenuLevel::Machines => vec![
MenuEntry::item('p', "Pushers...", |ed| ed.menu.push(MenuLevel::Pushers)),
@@ -88,56 +88,56 @@ impl MenuLevel {
ed.menu.push(MenuLevel::Transporters)
}),
MenuEntry::item('s', "/ CW spinner", |ed| {
ed.set_current(Archetype::Builtin(Builtin::Spinner, "spinner_cw"))
ed.set_current("spinner_cw")
}),
MenuEntry::item('c', "\\ CCW spinner", |ed| {
ed.set_current(Archetype::Builtin(Builtin::Spinner, "spinner_ccw"))
ed.set_current("spinner_ccw")
}),
],
MenuLevel::Pushers => vec![
MenuEntry::item('n', "▲ Pusher", |ed| {
ed.set_current(Archetype::Builtin(Builtin::Pusher, "pusher_north"))
ed.set_current("pusher_north")
}),
MenuEntry::item('s', "▼ Pusher", |ed| {
ed.set_current(Archetype::Builtin(Builtin::Pusher, "pusher_south"))
ed.set_current("pusher_south")
}),
MenuEntry::item('e', "► Pusher", |ed| {
ed.set_current(Archetype::Builtin(Builtin::Pusher, "pusher_east"))
ed.set_current("pusher_east")
}),
MenuEntry::item('w', "◄ Pusher", |ed| {
ed.set_current(Archetype::Builtin(Builtin::Pusher, "pusher_west"))
ed.set_current("pusher_west")
}),
],
MenuLevel::Transporters => vec![
MenuEntry::item('n', "▲ Transporter", |ed| {
ed.set_current(Archetype::Builtin(Builtin::Transporter, "transporter_north"))
ed.set_current("transporter_north")
}),
MenuEntry::item('s', "▼ Transporter", |ed| {
ed.set_current(Archetype::Builtin(Builtin::Transporter, "transporter_south"))
ed.set_current("transporter_south")
}),
MenuEntry::item('e', "► Transporter", |ed| {
ed.set_current(Archetype::Builtin(Builtin::Transporter, "transporter_east"))
ed.set_current("transporter_east")
}),
MenuEntry::item('w', "◄ Transporter", |ed| {
ed.set_current(Archetype::Builtin(Builtin::Transporter, "transporter_west"))
ed.set_current("transporter_west")
}),
],
MenuLevel::Items => vec![
MenuEntry::glyph('t', Archetype::Builtin(Builtin::Gem, "gem").default_glyph(), "Gem", |ed| {
MenuEntry::glyph('t', Builtin::Gem.default_glyph_for("gem"), "Gem", |ed| {
// 't' for 'treasure' — 'g' conflicts with glyph picker
ed.set_current(Archetype::Builtin(Builtin::Gem, "gem"))
ed.set_current("gem")
}),
MenuEntry::glyph('h', Archetype::Builtin(Builtin::Heart, "heart").default_glyph(), "Heart",
|ed| ed.set_current(Archetype::Builtin(Builtin::Heart, "heart"))),
MenuEntry::glyph('h', Builtin::Heart.default_glyph_for("heart"), "Heart",
|ed| ed.set_current("heart")),
MenuEntry::Separator,
MenuEntry::glyph('b', KeyType::Blue.glyph(), "Blue key", |ed| ed.set_current(Archetype::Builtin(Builtin::Key, "key_blue"))),
MenuEntry::glyph('g', KeyType::Green.glyph(),"Green key", |ed| ed.set_current(Archetype::Builtin(Builtin::Key, "key_green"))),
MenuEntry::glyph('c', KeyType::Cyan.glyph(),"Cyan key", |ed| ed.set_current(Archetype::Builtin(Builtin::Key, "key_cyan"))),
MenuEntry::glyph('r', KeyType::Red.glyph(),"Red key", |ed| ed.set_current(Archetype::Builtin(Builtin::Key, "key_red"))),
MenuEntry::glyph('p', KeyType::Purple.glyph(),"Purple key", |ed| ed.set_current(Archetype::Builtin(Builtin::Key, "key_purple"))),
MenuEntry::glyph('o', KeyType::Orange.glyph(), "Orange key", |ed| ed.set_current(Archetype::Builtin(Builtin::Key, "key_orange"))),
MenuEntry::glyph('y', KeyType::Yellow.glyph(), "Yellow key", |ed| ed.set_current(Archetype::Builtin(Builtin::Key, "key_yellow"))),
MenuEntry::glyph('w', KeyType::White.glyph(), "White key", |ed| ed.set_current(Archetype::Builtin(Builtin::Key, "key_white"))),
MenuEntry::glyph('b', KeyType::Blue.glyph(), "Blue key", |ed| ed.set_current("key_blue")),
MenuEntry::glyph('g', KeyType::Green.glyph(),"Green key", |ed| ed.set_current("key_green")),
MenuEntry::glyph('c', KeyType::Cyan.glyph(),"Cyan key", |ed| ed.set_current("key_cyan")),
MenuEntry::glyph('r', KeyType::Red.glyph(),"Red key", |ed| ed.set_current("key_red")),
MenuEntry::glyph('p', KeyType::Purple.glyph(),"Purple key", |ed| ed.set_current("key_purple")),
MenuEntry::glyph('o', KeyType::Orange.glyph(), "Orange key", |ed| ed.set_current("key_orange")),
MenuEntry::glyph('y', KeyType::Yellow.glyph(), "Yellow key", |ed| ed.set_current("key_yellow")),
MenuEntry::glyph('w', KeyType::White.glyph(), "White key", |ed| ed.set_current("key_white")),
],
}
}
+5 -4
View File
@@ -36,7 +36,8 @@ pub struct BoardWidget<'a> {
impl<'a> BoardWidget<'a> {
/// Creates a widget that renders `board`, scrolling to follow the player.
pub fn new(board: &'a Board) -> Self {
let focus = (board.player.x as i32, board.player.y as i32);
let (x, y) = board.player_pos();
let focus = (x as i32, y as i32);
Self { board, focus, fov: None }
}
@@ -180,11 +181,11 @@ mod tests {
use super::{BoardWidget, DARKNESS_BG};
use crate::utils::rgba8_to_color;
use kiln_core::Board;
use kiln_core::map_file::MapFile;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::widgets::Widget;
use kiln_core::board_spec::BoardSpec;
/// Builds a tiny dark board: a 5×1 corridor with the player at the left end
/// and a wall at x=2 occluding the two cells behind it.
@@ -201,7 +202,7 @@ mod tests {
"@" = { kind = "player" }
"#" = { kind = "wall", tile = "#", fg = "#808080", bg = "#404040" }
"##;
let mf: MapFile = toml::from_str(toml).unwrap();
let mf: BoardSpec = toml::from_str(toml).unwrap();
Board::try_from(mf).unwrap()
}
@@ -255,7 +256,7 @@ mod tests {
[grid.palette]
"L" = { kind = "object", tile = 1, fg = "#ff0000", bg = "#000000", solid = false, light = 2 }
"##;
let board = Board::try_from(toml::from_str::<MapFile>(toml).unwrap()).unwrap();
let board = Board::try_from(toml::from_str::<BoardSpec>(toml).unwrap()).unwrap();
let fov = board.lighting(0); // no player torch — only the object lights
let area = Rect::new(0, 0, 6, 1);
+10 -7
View File
@@ -113,11 +113,12 @@ impl Widget for SpeechBubblesWidget<'_> {
.bg(Color::Rgb(20, 20, 40));
// Determine the player's screen position for bubble avoidance.
let player = self.board.player_pos();
let (px, py, player_vis) = board_screen_pos(
area,
self.board,
self.board.player.x.max(0) as usize,
self.board.player.y.max(0) as usize,
player.0,
player.1,
);
let player = player_vis.then_some((px, py));
@@ -126,11 +127,12 @@ impl Widget for SpeechBubblesWidget<'_> {
.bubbles
.iter()
.filter_map(|b| {
let obj = self.board.objects.get(&b.object_id)?;
let (sx, sy, on_screen) = board_screen_pos(area, self.board, obj.x, obj.y);
let obj = self.board.get_hookable(b.object_id)?;
let (x, y) = obj.location();
let (sx, sy, on_screen) = board_screen_pos(area, self.board, x, y);
// On a dark board, a speaker the player can't see is treated like an
// off-screen speaker: the box still draws, but its tail is suppressed.
let visible = self.fov.is_none_or(|v| v.is_visible(obj.x, obj.y));
let visible = self.fov.is_none_or(|v| v.is_visible(x, y));
Some((sx, sy, on_screen && visible, b))
})
.collect();
@@ -423,8 +425,9 @@ fn center_top(obj_sy: u16, box_h: u16, area: Rect) -> u16 {
/// Returns `(sx, sy, on_screen)`. Off-screen coordinates are clamped to the viewport edge;
/// `on_screen` is `false` when clamping occurred (tail should be suppressed for that bubble).
fn board_screen_pos(area: Rect, board: &Board, bx: usize, by: usize) -> (u16, u16, bool) {
let (off_x, pad_x, _) = BoardWidget::axis(board.width, area.width as usize, board.player.x as i32);
let (off_y, pad_y, _) = BoardWidget::axis(board.height, area.height as usize, board.player.y as i32);
let player = board.player_pos();
let (off_x, pad_x, _) = BoardWidget::axis(board.width, area.width as usize, player.0 as i32);
let (off_y, pad_y, _) = BoardWidget::axis(board.height, area.height as usize, player.1 as i32);
let raw_sx = area.x as i32 + pad_x as i32 + (bx as i32 - off_x as i32);
let raw_sy = area.y as i32 + pad_y as i32 + (by as i32 - off_y as i32);
let sx = raw_sx.clamp(area.left() as i32, area.right() as i32 - 1) as u16;
+2 -2
View File
@@ -7,7 +7,7 @@
use std::collections::VecDeque;
use crate::utils::rgba8_to_color;
use kiln_core::{Archetype, Builtin};
use kiln_core::Builtin;
use kiln_core::cp437::tile_to_char;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
@@ -44,7 +44,7 @@ impl Widget for StatusSidebarWidget {
// Draw the gem indicator from the gem archetype's default glyph, so the
// sidebar matches what gems look like on the board (no hardcoded ♦/color).
let gem_glyph = Archetype::Builtin(Builtin::Gem, "gem").default_glyph();
let gem_glyph = Builtin::Gem.default_glyph_for("gem");
let gem_char = tile_to_char(gem_glyph.tile).to_string();
let gem_style = Style::default().fg(rgba8_to_color(gem_glyph.fg));