From 0241cd2a8d4996beae6039b8d177e92b96228a13 Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Sat, 30 May 2026 21:22:36 -0500 Subject: [PATCH] Move passable and opaque into objectdef --- kiln-core/src/game.rs | 46 +++++++++++++++++++++++---------------- kiln-core/src/map_file.rs | 12 ++++++++++ kiln-egui/src/editor.rs | 4 ++++ kiln-egui/src/main.rs | 14 +++++++----- maps/start.toml | 8 +++++++ 5 files changed, 59 insertions(+), 25 deletions(-) diff --git a/kiln-core/src/game.rs b/kiln-core/src/game.rs index ef9374a..841dcc9 100644 --- a/kiln-core/src/game.rs +++ b/kiln-core/src/game.rs @@ -72,7 +72,7 @@ pub struct FontSpec { /// /// For scripted objects (`Archetype::Object`), the behavior will eventually be /// determined by the Rhai script rather than this struct. See the Object variant -/// for details. +/// for details. (TODO fix this comment) #[derive(Copy, Clone, Debug)] pub struct Behavior { /// Whether an entity can walk through this cell. @@ -104,8 +104,6 @@ pub enum Archetype { Empty, /// A solid wall; impassable and opaque. Wall, - /// A scripted object. Behavior defaults to impassable until script-driven. - Object, /// Sentinel for map files that reference an unknown archetype name. /// Renders as a yellow `?` on red to make the error visible in-game. ErrorBlock, @@ -125,10 +123,6 @@ impl Archetype { passable: false, opaque: true, }, - Archetype::Object => Behavior { - passable: false, - opaque: false, - }, Archetype::ErrorBlock => Behavior { passable: false, opaque: true, @@ -141,7 +135,6 @@ impl Archetype { match self { Archetype::Empty => "empty", Archetype::Wall => "wall", - Archetype::Object => "object", Archetype::ErrorBlock => "error_block", } } @@ -162,11 +155,6 @@ impl Archetype { 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: 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, @@ -199,7 +187,7 @@ impl TryFrom<&str> for Archetype { /// /// `ErrorBlock` is excluded — it is a sentinel for load errors, not a valid /// editing choice. -pub const ALL_ARCHETYPES: &[Archetype] = &[Archetype::Empty, Archetype::Wall, Archetype::Object]; +pub const ALL_ARCHETYPES: &[Archetype] = &[Archetype::Empty, Archetype::Wall]; /// A scripted object placed on the board, loaded from a map file. /// @@ -228,11 +216,25 @@ pub struct ObjectDef { /// Visual representation of this object. Owned by the object (not derived /// from the grid cell), so scripts can change tile, fg, and bg at runtime. pub glyph: Glyph, + /// Whether the object blocks movement, of players or other objects + pub passable: bool, + /// Whether the object blocks line of sight / FOV for the player + pub opaque: bool, /// Name of the Rhai script in [`Board::scripts`] that drives this object. /// `None` means this object has no script yet. pub script_name: Option, } +impl ObjectDef { + pub fn default_glyph() -> Glyph { + Glyph { + tile: 63, + fg: Rgba8 { r: 255, g: 255, b: 0, a: 255 }, // yellow + bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 }, + } + } +} + /// A portal that teleports the player to a named entry point on another board. /// /// Portals are loaded from `[[portals]]` entries in `.toml` map files and @@ -345,17 +347,23 @@ impl Board { /// Objects block movement before the grid cell archetype is consulted. /// Panics if `x` or `y` are out of bounds. pub fn is_passable(&self, x: usize, y: usize) -> bool { - // An object on this cell always blocks (until scripting overrides this). - if self.object_at(x, y).is_some() { - return false; + // An object on this cell blocks if it's unpassable + if let Some(obj) = self.object_at(x, y) && !obj.passable { + false + } else { + self.get(x, y).1.behavior().passable } - self.get(x, y).1.behavior().passable } /// Returns the index into [`Board::objects`] of the object at `(x, y)`, if any. - pub fn object_at(&self, x: usize, y: usize) -> Option { + pub fn object_index_at(&self, x: usize, y: usize) -> Option { self.objects.iter().position(|o| o.x == x && o.y == y) } + + /// Returns a borrow of the actual object at `(x, y)` if any + pub fn object_at(&self, x: usize, y: usize) -> Option<&ObjectDef> { + self.object_index_at(x, y).map(|idx| &self.objects[idx]) + } } /// Holds the active game board and provides game-logic operations. diff --git a/kiln-core/src/map_file.rs b/kiln-core/src/map_file.rs index c1096ce..e957a16 100644 --- a/kiln-core/src/map_file.rs +++ b/kiln-core/src/map_file.rs @@ -138,11 +138,19 @@ pub(crate) struct MapFileObjectEntry { fg: String, /// Background color as an `"#RRGGBB"` hex string. bg: String, + /// Whether the object blocks player / object movement + #[serde(default)] + passable: bool, + /// Whether the object blocks player line of sight / FOV + #[serde(default = "default_true")] + opaque: bool, /// Name of the Rhai script in the `[scripts]` table, if any. #[serde(default, skip_serializing_if = "Option::is_none")] script_name: Option, } +fn default_true() -> bool { true } + /// Parses an `"#RRGGBB"` hex color string into an [`Rgba8`]. /// Returns opaque black on any parse failure. fn parse_color(hex: &str) -> Rgba8 { @@ -256,6 +264,8 @@ impl TryFrom for Board { fg: parse_color(&e.fg), bg: parse_color(&e.bg), }, + passable: e.passable, + opaque: e.opaque, script_name: e.script_name, }) .collect(); @@ -365,6 +375,8 @@ impl From<&Board> for MapFile { tile: TileIndex::Num(o.glyph.tile), fg: color_to_hex(o.glyph.fg), bg: color_to_hex(o.glyph.bg), + passable: o.passable, + opaque: o.opaque, script_name: o.script_name.clone(), }) .collect(); diff --git a/kiln-egui/src/editor.rs b/kiln-egui/src/editor.rs index 7c95050..3659286 100644 --- a/kiln-egui/src/editor.rs +++ b/kiln-egui/src/editor.rs @@ -180,6 +180,10 @@ pub(crate) fn show_editor_panel( } let obj = &mut board.objects[i]; + + ui.checkbox(&mut obj.passable, "Passable"); + ui.checkbox(&mut obj.opaque, "Opaque"); + let current = obj.script_name.as_deref().unwrap_or("(none)").to_string(); diff --git a/kiln-egui/src/main.rs b/kiln-egui/src/main.rs index 6b3a21d..f9d2306 100644 --- a/kiln-egui/src/main.rs +++ b/kiln-egui/src/main.rs @@ -10,7 +10,7 @@ mod render; use editor::{EditorState, EditorTab, show_editor_panel}; use font::BitmapFont; -use kiln_core::game::{Archetype, FontSpec, GameState, Glyph, ObjectDef}; +use kiln_core::game::{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, @@ -236,22 +236,24 @@ impl eframe::App for App { if self.editor.tab == EditorTab::Objects { if self.editor.placing_object { // Placement mode: create a new object here if none exists. - if board.object_at(cx, cy).is_none() { + if board.object_index_at(cx, cy).is_none() { board.objects.push(ObjectDef { x: cx, y: cy, glyph: Glyph { - tile: Archetype::Object.default_glyph().tile, - fg: Archetype::Object.default_glyph().fg, - bg: Archetype::Object.default_glyph().bg, + tile: ObjectDef::default_glyph().tile, + fg: ObjectDef::default_glyph().fg, + bg: ObjectDef::default_glyph().bg, }, + passable: false, + opaque: true, script_name: None, }); } self.editor.placing_object = false; } else { // Selection mode: click to select an existing object. - let found = board.object_at(cx, cy); + let found = board.object_index_at(cx, cy); self.editor.selected_object = found; self.editor.object_glyph_picker_open = false; if let Some(i) = found { diff --git a/maps/start.toml b/maps/start.toml index e420893..e0819ae 100644 --- a/maps/start.toml +++ b/maps/start.toml @@ -37,3 +37,11 @@ content = """ # # ############################################################ """ + +[[objects]] +fg = "#aa3333" +bg = "#000000" +x = 10 +y = 10 +tile = "#" +passable = true \ No newline at end of file