player object

This commit is contained in:
2026-06-25 00:04:42 -05:00
parent d8a3f17379
commit db8c8e615d
15 changed files with 207 additions and 168 deletions
+2 -2
View File
@@ -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),
+4 -4
View File
@@ -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<Layer>,
/// 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,
},
+22 -120
View File
@@ -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<f64>,
/// 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]
+85
View File
@@ -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),
]
}
}
+2
View File
@@ -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;
+2 -2
View File
@@ -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<MapFile> for Board {
width: w,
height: h,
layers,
player: Player {
player: PlayerPos {
x: px as i32,
y: py as i32,
},
+49
View File
@@ -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(),
}
}
}
+23 -8
View File
@@ -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<ScriptArg>) {
pub(crate) fn run_send(&mut self, player: Player, target_id: ObjectId, fn_name: &str, arg: Option<ScriptArg>) {
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>("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) {
+3 -3
View File
@@ -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<PortalDef>) -> 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");
+2 -2
View File
@@ -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(),
+1 -1
View File
@@ -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).
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+7 -22
View File
@@ -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<Span> = (0..MAX_HEARTS)
let filled = player.health;
let hearts: Vec<Span> = (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()),
+3 -2
View File
@@ -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.");