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
+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]