player object
This commit is contained in:
@@ -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,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
@@ -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]
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
@@ -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,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");
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user