player object
This commit is contained in:
+22
-120
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user