diff --git a/kiln-core/src/archetype.rs b/kiln-core/src/archetype.rs index 87f6d03..becd5f2 100644 --- a/kiln-core/src/archetype.rs +++ b/kiln-core/src/archetype.rs @@ -1,7 +1,7 @@ use crate::glyph::Glyph; use crate::utils::{Behavior, Pushable}; use color::Rgba8; -use crate::game::KeyType; +use crate::keys::KeyType; /// Declares the set of script-backed archetype families. /// @@ -353,7 +353,7 @@ mod tests { #[test] fn key_aliases_have_distinct_fg_colors() { - use crate::game::KeyType; + use crate::keys::KeyType; // Each alias must match the corresponding KeyType glyph — single source of truth. let cases = [ ("key_blue", KeyType::Blue), diff --git a/kiln-core/src/board.rs b/kiln-core/src/board.rs index 9b92bb2..6ea70b0 100644 --- a/kiln-core/src/board.rs +++ b/kiln-core/src/board.rs @@ -4,7 +4,7 @@ use crate::layer::Layer; use crate::log::LogLine; use crate::object_def::ObjectDef; use crate::utils::Direction; -use crate::utils::{Behavior, ObjectId, Player, PortalDef, Pushable, RegistryValue, Solid}; +use crate::utils::{Behavior, ObjectId, PlayerPos, PortalDef, Pushable, RegistryValue, Solid}; use std::collections::{BTreeMap, HashMap, HashSet}; /// The complete state of one game board (a single room or screen). @@ -44,7 +44,7 @@ pub struct Board { /// cell with [`Board::get`]/[`Board::get_mut`] by `(z, x, y)`. pub(crate) layers: Vec, /// Current player position. See [`Player`] for caveats about its future. - pub player: Player, + pub player: PlayerPos, /// Scripted objects on this board, keyed by stable [`ObjectId`]. A `BTreeMap` /// (not a `Vec`) so an object can be removed without invalidating other /// objects' ids; iteration is in ascending-id order, which equals load order @@ -623,7 +623,7 @@ pub(crate) mod tests { use crate::layer::Layer; use crate::object_def::ObjectDef; use crate::utils::Direction; - use crate::utils::{ObjectId, Player}; + use crate::utils::{ObjectId, PlayerPos}; use color::Rgba8; use std::collections::{BTreeMap, HashMap}; @@ -653,7 +653,7 @@ pub(crate) mod tests { layers: vec![Layer { cells: vec![(Glyph::transparent(), Archetype::Empty); w * h], }], - player: Player { + player: PlayerPos { x: player.0, y: player.1, }, diff --git a/kiln-core/src/game.rs b/kiln-core/src/game.rs index a3b163f..84166dc 100644 --- a/kiln-core/src/game.rs +++ b/kiln-core/src/game.rs @@ -2,102 +2,18 @@ use crate::action::Action; use crate::board::Board; use crate::log::LogLine; use crate::script::ScriptHost; -use crate::utils::{Direction, ObjectId, Player, ScriptArg}; +use crate::utils::{Direction, ObjectId, PlayerPos, ScriptArg}; use crate::world::World; -use color::Rgba8; use std::cell::{Ref, RefMut}; use std::time::Duration; /// How long a `say()` speech bubble stays on screen, in seconds. pub const SAY_DURATION: f64 = 3.0; -#[derive(Copy, Clone, Debug)] -pub enum KeyType { - Red, Orange, Yellow, Green, Blue, Cyan, Purple, White -} - -impl KeyType { - pub fn glyph(self) -> Glyph { - let fg = match self { - KeyType::Red => Rgba8 { r: 0xFF, g: 0x50, b: 0x50, a: 255 }, - KeyType::Orange => Rgba8 { r: 0xFF, g: 0x88, b: 0x00, a: 255 }, - KeyType::Yellow => Rgba8 { r: 0xFF, g: 0xFF, b: 0x50, a: 255 }, - KeyType::Green => Rgba8 { r: 0x50, g: 0xFF, b: 0x50, a: 255 }, - KeyType::Blue => Rgba8 { r: 0x50, g: 0x50, b: 0xFF, a: 255 }, - KeyType::Cyan => Rgba8 { r: 0x00, g: 0xAA, b: 0xAA, a: 255 }, - KeyType::Purple => Rgba8 { r: 0xAA, g: 0x00, b: 0xAA, a: 255 }, - KeyType::White => Rgba8 { r: 0xFF, g: 0xFF, b: 0xFF, a: 255 } - }; - - Glyph { tile: 12, fg, bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 } } - } -} - -/// The player's key inventory: one boolean per key color. -/// -/// Each field is `true` when the player holds a key of that color. Use -/// [`colors`](Keyring::colors) to iterate all eight in display order, each -/// paired with its [`Rgba8`] color for rendering. -#[derive(Copy, Clone, Default)] -pub struct Keyring { - /// Red key. - pub red: bool, - /// Orange key (custom color, not in the standard EGA palette). - pub orange: bool, - /// Yellow key. - pub yellow: bool, - /// Green key. - pub green: bool, - /// Blue key. - pub blue: bool, - /// Cyan key. - pub cyan: bool, - /// Purple key. - pub purple: bool, - /// White key. - pub white: bool, -} - -impl Keyring { - /// Sets the named key slot to `value`. Returns `false` if `name` is not a - /// recognized color (`"blue"`, `"green"`, `"cyan"`, `"red"`, `"purple"`, - /// `"orange"`, `"yellow"`, `"white"`). - pub fn set_by_name(&mut self, name: &str, value: bool) -> bool { - match name { - "blue" => self.blue = value, - "green" => self.green = value, - "cyan" => self.cyan = value, - "red" => self.red = value, - "purple" => self.purple = value, - "orange" => self.orange = value, - "yellow" => self.yellow = value, - "white" => self.white = value, - _ => return false, - } - true - } - - /// Returns all eight keys in display order, each paired with its color. - /// - /// Order: blue, green, cyan, red, purple, orange, yellow, white. - pub fn colors(&self) -> [(bool, Rgba8); 8] { - [ - (self.blue, KeyType::Blue.glyph().fg), - (self.green, KeyType::Green.glyph().fg), - (self.cyan, KeyType::Cyan.glyph().fg), - (self.red, KeyType::Red.glyph().fg), - (self.purple, KeyType::Purple.glyph().fg), - (self.orange, KeyType::Orange.glyph().fg), - (self.yellow, KeyType::Yellow.glyph().fg), - (self.white, KeyType::White.glyph().fg), - ] - } -} - // Re-export ScrollLine so kiln-tui can pattern-match scroll content without // accessing the private `action` module directly. pub use crate::action::ScrollLine; -use crate::glyph::Glyph; +use crate::player::Player; /// An active scroll overlay opened by a scripted object via `scroll()`. /// @@ -151,18 +67,8 @@ pub struct GameState { /// by [`enter_board`](GameState::enter_board). Front-ends tick this down and /// may block input or show a visual effect while it is `Some(t)` where `t > 0`. pub board_transition: Option, - /// The player's current health. Game-global (not per-board), so it persists - /// across board transitions. Scripts change it via `alter_health(dh)`, which - /// clamps to `[0, max_health]`. Front-ends display it via the status sidebar. - pub player_health: u32, - /// The maximum health the player can have. Scripts clamp `alter_health` to - /// this ceiling; the status sidebar uses it to size the heart row. - pub max_health: u32, - /// The number of gems the player has collected. Game-global like - /// [`player_health`](GameState::player_health); starts at `0`. - pub player_gems: u32, - /// The player's key inventory. Game-global; persists across board transitions. - pub player_keys: Keyring, + /// The player's state + pub player: Player, } impl GameState { @@ -189,10 +95,7 @@ impl GameState { speech_bubbles: Vec::new(), active_scroll: None, board_transition: None, - player_health: 5, - max_health: 5, - player_gems: 0, - player_keys: Keyring::default(), + player: Player::default() }; state.drain_errors(); state @@ -256,7 +159,7 @@ impl GameState { /// the game is about to start — never during map deserialization, since a script /// may inspect the board. pub fn run_init(&mut self) { - self.scripts.run_init(); + self.scripts.run_init(self.player); self.resolve(); } @@ -270,7 +173,7 @@ impl GameState { // this runs exactly once per player interaction with a scroll. self.handle_scroll(); let secs = dt.as_secs_f64(); - self.scripts.run_tick(secs); + self.scripts.run_tick(self.player, secs); // Expire speech bubbles before resolving new actions so a fresh say() // this frame isn't immediately culled. self.speech_bubbles.retain_mut(|b| { @@ -430,23 +333,22 @@ impl GameState { } // Apply the net gem change (clamped at 0, since the count is unsigned). if gem_delta != 0 { - self.player_gems = (self.player_gems as i64 + gem_delta).max(0) as u32; + self.player.alter_gems(gem_delta); } // Apply the net health change (clamped to [0, max_health]). if health_delta != 0 { - self.player_health = (self.player_health as i64 + health_delta) - .clamp(0, self.max_health as i64) as u32; + self.player.alter_health(health_delta); } for (color, present) in key_changes { - if !self.player_keys.set_by_name(&color, present) { + if !self.player.keys.set_by_name(&color, present) { self.log.push(LogLine::error(format!("set_key: unknown color {color:?}"))); } } for (bumped, bumper) in bumps { - self.scripts.run_bump(bumped, bumper); + self.scripts.run_bump(self.player, bumped, bumper); } for (target, fn_name, arg) in sends { - self.scripts.run_send(target, &fn_name, arg); + self.scripts.run_send(self.player, target, &fn_name, arg); } self.drain_errors(); } @@ -462,7 +364,7 @@ impl GameState { if let Some(scroll) = self.active_scroll.take() && let Some(choice) = scroll.choice { - self.scripts.run_send(scroll.source, &choice, None); + self.scripts.run_send(self.player, scroll.source, &choice, None); self.drain_errors(); } } @@ -501,7 +403,7 @@ impl GameState { self.active_scroll = None; // Switch to the new board and place the player at the arrival portal. self.current_board_name = target_map.to_string(); - self.board_mut().player = Player { + self.board_mut().player = PlayerPos { x: ax as i32, y: ay as i32, }; @@ -569,11 +471,11 @@ impl GameState { // Fire the grab hook and resolve it immediately so the grabbed thing's // die()/add_gems() apply now — no player+object overlap survives this call. if let Some(id) = grabbed { - self.scripts.run_grab(id); + self.scripts.run_grab(self.player, id); self.resolve(); } if let Some(idx) = bumped { - self.scripts.run_bump(idx, -1); + self.scripts.run_bump(self.player, idx, -1); self.drain_errors(); } } @@ -629,7 +531,7 @@ mod tests { game.try_move(Direction::East); // The gem was grabbed: gem count up, gem object gone, player on its cell. - assert_eq!(game.player_gems, 1); + assert_eq!(game.player.gems, 1); assert!(game.board().objects.is_empty()); assert_eq!((game.board().player.x, game.board().player.y), (1, 0)); } @@ -657,7 +559,7 @@ mod tests { game.tick(Duration::from_millis(16)); // No grab: the gem is untouched and the player never moved. - assert_eq!(game.player_gems, 0); + assert_eq!(game.player.gems, 0); assert!(game.board().objects.values().any(|o| o.grab)); assert_eq!((game.board().player.x, game.board().player.y), (2, 0)); } @@ -835,9 +737,9 @@ mod tests { let mut game = GameState::with_scripts(board, scripts); game.run_init(); - assert!(game.player_keys.blue); - assert!(game.player_keys.red); - assert!(!game.player_keys.cyan); // cyan was not set by the script + assert!(game.player.keys.blue); + assert!(game.player.keys.red); + assert!(!game.player.keys.cyan); // cyan was not set by the script // A second script can take a key. let mut sobj2 = ObjectDef::new(0, 0); @@ -851,7 +753,7 @@ mod tests { let mut game2 = GameState::with_scripts(board2, scripts2); game2.run_init(); - assert!(!game2.player_keys.blue); + assert!(!game2.player.keys.blue); } #[test] diff --git a/kiln-core/src/keys.rs b/kiln-core/src/keys.rs new file mode 100644 index 0000000..74cf4b9 --- /dev/null +++ b/kiln-core/src/keys.rs @@ -0,0 +1,85 @@ +use color::Rgba8; +use crate::glyph::Glyph; + +#[derive(Copy, Clone, Debug)] +pub enum KeyType { + Red, Orange, Yellow, Green, Blue, Cyan, Purple, White +} + +impl KeyType { + pub fn glyph(self) -> Glyph { + let fg = match self { + KeyType::Red => Rgba8 { r: 0xFF, g: 0x50, b: 0x50, a: 255 }, + KeyType::Orange => Rgba8 { r: 0xFF, g: 0x88, b: 0x00, a: 255 }, + KeyType::Yellow => Rgba8 { r: 0xFF, g: 0xFF, b: 0x50, a: 255 }, + KeyType::Green => Rgba8 { r: 0x50, g: 0xFF, b: 0x50, a: 255 }, + KeyType::Blue => Rgba8 { r: 0x50, g: 0x50, b: 0xFF, a: 255 }, + KeyType::Cyan => Rgba8 { r: 0x00, g: 0xAA, b: 0xAA, a: 255 }, + KeyType::Purple => Rgba8 { r: 0xAA, g: 0x00, b: 0xAA, a: 255 }, + KeyType::White => Rgba8 { r: 0xFF, g: 0xFF, b: 0xFF, a: 255 } + }; + + Glyph { tile: 12, fg, bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 } } + } +} + +/// The player's key inventory: one boolean per key color. +/// +/// Each field is `true` when the player holds a key of that color. Use +/// [`colors`](Keyring::colors) to iterate all eight in display order, each +/// paired with its [`Rgba8`] color for rendering. +#[derive(Copy, Clone, Default)] +pub struct Keyring { + /// Red key. + pub red: bool, + /// Orange key (custom color, not in the standard EGA palette). + pub orange: bool, + /// Yellow key. + pub yellow: bool, + /// Green key. + pub green: bool, + /// Blue key. + pub blue: bool, + /// Cyan key. + pub cyan: bool, + /// Purple key. + pub purple: bool, + /// White key. + pub white: bool, +} + +impl Keyring { + /// Sets the named key slot to `value`. Returns `false` if `name` is not a + /// recognized color (`"blue"`, `"green"`, `"cyan"`, `"red"`, `"purple"`, + /// `"orange"`, `"yellow"`, `"white"`). + pub fn set_by_name(&mut self, name: &str, value: bool) -> bool { + match name { + "blue" => self.blue = value, + "green" => self.green = value, + "cyan" => self.cyan = value, + "red" => self.red = value, + "purple" => self.purple = value, + "orange" => self.orange = value, + "yellow" => self.yellow = value, + "white" => self.white = value, + _ => return false, + } + true + } + + /// Returns all eight keys in display order, each paired with its color. + /// + /// Order: blue, green, cyan, red, purple, orange, yellow, white. + pub fn colors(&self) -> [(bool, Rgba8); 8] { + [ + (self.blue, KeyType::Blue.glyph().fg), + (self.green, KeyType::Green.glyph().fg), + (self.cyan, KeyType::Cyan.glyph().fg), + (self.red, KeyType::Red.glyph().fg), + (self.purple, KeyType::Purple.glyph().fg), + (self.orange, KeyType::Orange.glyph().fg), + (self.yellow, KeyType::Yellow.glyph().fg), + (self.white, KeyType::White.glyph().fg), + ] + } +} \ No newline at end of file diff --git a/kiln-core/src/lib.rs b/kiln-core/src/lib.rs index f8981ce..a9c9133 100644 --- a/kiln-core/src/lib.rs +++ b/kiln-core/src/lib.rs @@ -22,6 +22,8 @@ pub mod script; mod utils; /// World type: a named collection of boards in a single `.toml` file ([`world::World`], [`world::load`]). pub mod world; +pub mod player; +pub mod keys; pub use archetype::{Archetype, Builtin}; pub use board::Board; diff --git a/kiln-core/src/map_file.rs b/kiln-core/src/map_file.rs index 377dbaf..29909d5 100644 --- a/kiln-core/src/map_file.rs +++ b/kiln-core/src/map_file.rs @@ -26,7 +26,7 @@ use crate::glyph::Glyph; use crate::layer::{Layer, LayerData, PaletteEntry, Placement, build_layer}; use crate::log::LogLine; use crate::object_def::ObjectDef; -use crate::utils::{ObjectId, Player, PortalDef}; +use crate::utils::{ObjectId, PlayerPos, PortalDef}; /// The serde shell for one board in a `.toml` file: a header and its layers. /// @@ -277,7 +277,7 @@ impl TryFrom for Board { width: w, height: h, layers, - player: Player { + player: PlayerPos { x: px as i32, y: py as i32, }, diff --git a/kiln-core/src/player.rs b/kiln-core/src/player.rs new file mode 100644 index 0000000..71a0bf4 --- /dev/null +++ b/kiln-core/src/player.rs @@ -0,0 +1,49 @@ +use crate::keys::Keyring; + +#[derive(Copy, Clone)] +pub struct Player { + /// The player's current health. Game-global (not per-board), so it persists + /// across board transitions. Scripts change it via `alter_health(dh)`, which + /// clamps to `[0, max_health]`. Front-ends display it via the status sidebar. + pub health: i64, + + /// The maximum health the player can have. Scripts clamp `alter_health` to + /// this ceiling; the status sidebar uses it to size the heart row. + pub max_health: i64, + + /// The player's key inventory. Game-global; persists across board transitions. + pub keys: Keyring, + + /// The number of gems the player has collected. Game-global like + /// [`player_health`](GameState::player_health); starts at `0`. + pub gems: i64, +} + +impl Player { + /// Attempt to modify the gem total by the given amount, but maintain a minimum of zero. + /// If the modification would take up below zero, leave it alone and return false. + pub fn alter_gems(&mut self, delta: i64) -> bool { + if delta + self.gems < 0 { + false + } else { + self.gems += delta; + true + } + } + + /// Modify the player's health by the given delta, clamped to within the range `[0, max_health]` + pub fn alter_health(&mut self, delta: i64) { + self.health = (self.health + delta).clamp(0, self.max_health); + } +} + +impl Default for Player { + fn default() -> Player { + Self { + health: 5, + max_health: 5, + gems: 0, + keys: Keyring::default(), + } + } +} \ No newline at end of file diff --git a/kiln-core/src/script.rs b/kiln-core/src/script.rs index cb03165..4f77aab 100644 --- a/kiln-core/src/script.rs +++ b/kiln-core/src/script.rs @@ -67,7 +67,7 @@ use crate::action::{Action, MOVE_COST, ScrollLine, action_to_map}; use crate::board::Board; -use crate::game::SAY_DURATION; +use crate::game::{SAY_DURATION}; use crate::log::LogLine; use crate::map_file::{color_to_hex, parse_color}; use crate::object_def::ObjectDef; @@ -79,6 +79,7 @@ use rhai::{ use std::cell::RefCell; use std::collections::{HashMap, HashSet, VecDeque}; use std::rc::Rc; +use crate::player::Player; /// An action promoted from an object's output queue onto the board queue, tagged /// with the object that issued it. @@ -229,6 +230,7 @@ impl ScriptHost { register_glyph_type(&mut engine); register_registry_type(&mut engine); register_global_constants(&mut engine); + register_player_type(&mut engine); let board = board_cell.borrow(); @@ -316,18 +318,18 @@ impl ScriptHost { } /// Calls `init()` on every scripted object that defines it and drains each queue. - pub fn run_init(&mut self) { - self.run("init", |c| c.has_init, (), 0.0); + pub fn run_init(&mut self, player: Player) { + self.run("init", |c| c.has_init, (), 0.0, player); } /// Calls `tick(dt)` on every scripted object that defines it, then drains queues. - pub fn run_tick(&mut self, dt: f64) { - self.run("tick", |c| c.has_tick, (dt,), dt); + pub fn run_tick(&mut self, player: Player, dt: f64) { + self.run("tick", |c| c.has_tick, (dt,), dt, player); } /// Calls `bump(id)` on the object with [`ObjectId`] `object_id`, if it defines /// the hook. After the hook, drains the object's queue with `dt = 0`. - pub fn run_bump(&mut self, object_id: ObjectId, bumper: i64) { + pub fn run_bump(&mut self, player: Player, object_id: ObjectId, bumper: i64) { let Some(i) = self.objects.iter().position(|o| o.object_id == object_id) else { return; }; @@ -347,6 +349,7 @@ impl ScriptHost { return; } let options = CallFnOptions::default().with_tag(object_id as i64); + obj.scope.set_or_push("Player", player); if let Err(err) = engine.call_fn_with_options::<()>( options, &mut obj.scope, @@ -369,7 +372,7 @@ impl ScriptHost { /// Fired when the player walks onto a grab object or a grab object is pushed /// into the player (see [`GameState`](crate::game::GameState)). The hook /// typically increments a player stat and removes the object via `die()`. - pub fn run_grab(&mut self, object_id: ObjectId) { + pub fn run_grab(&mut self, player: Player, object_id: ObjectId) { let Some(i) = self.objects.iter().position(|o| o.object_id == object_id) else { return; }; @@ -389,6 +392,7 @@ impl ScriptHost { return; } let options = CallFnOptions::default().with_tag(object_id as i64); + obj.scope.set_or_push("Player", player); if let Err(err) = engine.call_fn_with_options::<()>( options, &mut obj.scope, @@ -411,7 +415,7 @@ impl ScriptHost { /// If the function accepts zero parameters (or arg is `None`), it is called with /// no args. If neither arity exists, the call is silently skipped. /// After the call, drains the object's queue with `dt = 0`. - pub(crate) fn run_send(&mut self, target_id: ObjectId, fn_name: &str, arg: Option) { + pub(crate) fn run_send(&mut self, player: Player, target_id: ObjectId, fn_name: &str, arg: Option) { let Some(i) = self.objects.iter().position(|o| o.object_id == target_id) else { return; }; @@ -438,6 +442,7 @@ impl ScriptHost { .any(|f| f.name == fn_name && f.params.is_empty()); let options = CallFnOptions::default().with_tag(obj.object_id as i64); + obj.scope.set_or_push("Player", player); let result = if has_1 { // Call with arg (or unit if arg is absent). let dyn_arg: Dynamic = match &arg { @@ -525,6 +530,7 @@ impl ScriptHost { defined: fn(&CompiledScript) -> bool, args: A, drain_dt: f64, + player: Player ) { for i in 0..self.objects.len() { { @@ -540,6 +546,7 @@ impl ScriptHost { && defined(compiled) { let options = CallFnOptions::default().with_tag(obj.object_id as i64); + obj.scope.set_or_push("Player", player); if let Err(err) = engine.call_fn_with_options::<()>( options, &mut obj.scope, @@ -559,6 +566,14 @@ impl ScriptHost { } } +fn register_player_type(engine: &mut Engine) { + // TODO also expose keys somehow + engine.register_type_with_name::("Player") + .register_get("gems", |player: &mut Player| player.gems) + .register_get("health", |player: &mut Player| player.health) + .register_get("max_health", |player: &mut Player| player.max_health); +} + // ── Register Me type ────────────────────────────────────────────────────────── fn register_me_type(engine: &mut Engine) { diff --git a/kiln-core/src/tests/game_portals.rs b/kiln-core/src/tests/game_portals.rs index 201706b..891e895 100644 --- a/kiln-core/src/tests/game_portals.rs +++ b/kiln-core/src/tests/game_portals.rs @@ -3,7 +3,7 @@ use crate::board::Board; use crate::game::GameState; use crate::glyph::Glyph; use crate::layer::Layer; -use crate::utils::{Direction, Player, PortalDef}; +use crate::utils::{Direction, PlayerPos, PortalDef}; use crate::world::World; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap}; @@ -18,7 +18,7 @@ fn make_board(px: i32, py: i32, portals: Vec) -> Board { layers: vec![Layer { cells: vec![(Glyph::transparent(), Archetype::Empty); 9], }], - player: Player { x: px, y: py }, + player: PlayerPos { x: px, y: py }, objects: BTreeMap::new(), next_object_id: 1, portals, @@ -112,7 +112,7 @@ fn enter_board_unknown_entry_logs_error() { fn try_move_onto_portal_switches_board() { let world = two_board_world(); // Start the player one cell west of the portal at (2, 0). - world.boards["b1"].borrow_mut().player = Player { x: 1, y: 0 }; + world.boards["b1"].borrow_mut().player = PlayerPos { x: 1, y: 0 }; let mut game = GameState::from_world(world); game.try_move(Direction::East); assert_eq!(game.current_board_name(), "b2"); diff --git a/kiln-core/src/tests/mod.rs b/kiln-core/src/tests/mod.rs index ffdae08..6f47c80 100644 --- a/kiln-core/src/tests/mod.rs +++ b/kiln-core/src/tests/mod.rs @@ -11,7 +11,7 @@ use crate::game::GameState; use crate::glyph::Glyph; use crate::layer::Layer; use crate::object_def::ObjectDef; -use crate::utils::Player; +use crate::utils::PlayerPos; use std::collections::{BTreeMap, HashMap}; /// Builds a 1×1 board with a single object that optionally references a script, @@ -32,7 +32,7 @@ fn board_with_object( layers: vec![Layer { cells: vec![(Glyph::transparent(), Archetype::Empty)], }], - player: Player { x: 0, y: 0 }, + player: PlayerPos { x: 0, y: 0 }, objects: BTreeMap::from([(1, object)]), next_object_id: 2, portals: Vec::new(), diff --git a/kiln-core/src/utils.rs b/kiln-core/src/utils.rs index b24a71b..a287ed7 100644 --- a/kiln-core/src/utils.rs +++ b/kiln-core/src/utils.rs @@ -291,7 +291,7 @@ impl PortalDef { /// See the "player may become an object" notes in `CLAUDE.md` for the design /// tensions this implies. #[derive(Copy, Clone)] -pub struct Player { +pub struct PlayerPos { /// Column position (0-indexed, increasing rightward). pub x: i32, /// Row position (0-indexed, increasing downward). diff --git a/kiln-tui/src/editor_menu.rs b/kiln-tui/src/editor_menu.rs index dd57064..b3f23f4 100644 --- a/kiln-tui/src/editor_menu.rs +++ b/kiln-tui/src/editor_menu.rs @@ -2,7 +2,7 @@ use crate::editor::EditorState; use crate::menu::{MenuItem, MenuKey}; use crate::mode::PendingMode; use kiln_core::{Archetype, Builtin}; -use kiln_core::game::KeyType; +use kiln_core::keys::KeyType; use kiln_core::glyph::Glyph; /// One level in the editor's sidebar menu tree. diff --git a/kiln-tui/src/main.rs b/kiln-tui/src/main.rs index 6bebb21..403857a 100644 --- a/kiln-tui/src/main.rs +++ b/kiln-tui/src/main.rs @@ -342,7 +342,7 @@ fn draw_play(frame: &mut Frame, game: &GameState, ui: &mut Ui) { .spacing(Spacing::Overlap(1)) .areas(frame.area()); frame.render_widget( - StatusSidebarWidget::new(game.player_health, game.player_gems, game.player_keys), + StatusSidebarWidget(game.player), sidebar_area, ); rest diff --git a/kiln-tui/src/status.rs b/kiln-tui/src/status.rs index 89bc641..552df1a 100644 --- a/kiln-tui/src/status.rs +++ b/kiln-tui/src/status.rs @@ -7,7 +7,6 @@ use std::collections::VecDeque; use crate::utils::rgba8_to_color; -use kiln_core::game::Keyring; use kiln_core::{Archetype, Builtin}; use kiln_core::cp437::tile_to_char; use ratatui::buffer::Buffer; @@ -16,9 +15,8 @@ use ratatui::style::{Color, Style}; use ratatui::symbols::merge::MergeStrategy; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph, Widget}; +use kiln_core::player::Player; -/// How many hearts the health row always shows (the player's max health). -const MAX_HEARTS: u32 = 5; /// A filled heart: bright red. const HEART_FULL: Color = Color::Rgb(255, 0, 0); /// A depleted heart: dark red. @@ -29,28 +27,15 @@ const HEART_EMPTY: Color = Color::Rgb(90, 0, 0); /// Shows a `Health:` label above a row of [`MAX_HEARTS`] heart glyphs, a /// `Gems: ♦ N` line, and a `Keys:` row of 8 `♀` glyphs colored when held /// and dark gray when absent. -pub struct StatusSidebarWidget { - /// The player's current health, clamped to [`MAX_HEARTS`] when drawn. - health: u32, - /// The number of gems collected, shown as a count. - gems: u32, - /// The player's key inventory. - keys: Keyring, -} - -impl StatusSidebarWidget { - /// Builds a sidebar widget for the given player stats. - pub fn new(health: u32, gems: u32, keys: Keyring) -> Self { - Self { health, gems, keys } - } -} +pub struct StatusSidebarWidget(pub Player); impl Widget for StatusSidebarWidget { fn render(self, area: Rect, buf: &mut Buffer) { + let player = self.0; // One bright-red span per filled heart, dark-red for the rest, so a // partly-depleted bar reads at a glance. - let filled = self.health.min(MAX_HEARTS); - let hearts: Vec = (0..MAX_HEARTS) + let filled = player.health; + let hearts: Vec = (0..player.max_health) .map(|i| { let color = if i < filled { HEART_FULL } else { HEART_EMPTY }; Span::styled("♥", Style::default().fg(color)) @@ -64,7 +49,7 @@ impl Widget for StatusSidebarWidget { let gem_style = Style::default().fg(rgba8_to_color(gem_glyph.fg)); // Build the key row: 8 ♀ glyphs, colored when held, near-black when absent. - let mut key_spans: VecDeque<_> = self + let mut key_spans: VecDeque<_> = player .keys .colors() .iter() @@ -86,7 +71,7 @@ impl Widget for StatusSidebarWidget { Line::from(vec![ Span::raw("Gems: "), Span::styled(gem_char, gem_style), - Span::raw(format!("{} ", self.gems)), + Span::raw(format!("{} ", player.gems)), ]), Line::from(""), Line::from_iter(key_spans.into_iter()), diff --git a/maps/start.toml b/maps/start.toml index 9fb73ba..a4339db 100644 --- a/maps/start.toml +++ b/maps/start.toml @@ -10,6 +10,7 @@ fn init() { log(`hello from object — player at ${Board.player_x}, ${Board.player_y}`); set_tile(2); // change my glyph to ☻ — proves the write path say("Hello there,\\ntraveller!"); + log(`Player health: ${Player.health}`); } fn tick(dt) { @@ -73,8 +74,8 @@ fn bump(id) { ]); } fn eat() { - log("You ate the muffin. Delicious."); - say("Mmm!"); + say("Yeah it was poisoned."); + alter_health(-2); } fn ignore() { log("You walk away from the muffin. Wise.");