heart containers

This commit is contained in:
2026-06-23 22:52:25 -05:00
parent 8637c0a52a
commit d8a3f17379
11 changed files with 58 additions and 20 deletions
+7
View File
@@ -79,6 +79,9 @@ pub(crate) enum Action {
/// Add `n` to the player's gem count (negative subtracts; the count is
/// clamped at 0). Zero time cost. Applied to `GameState::player_gems`.
AddGems(i64),
/// Add `dh` to the player's health (clamped to `[0, max_health]`). Zero time
/// cost. Applied to `GameState::player_health`.
AlterHealth(i64),
/// Give (`true`) or take (`false`) the named key color from the player.
///
/// Color must be one of `"blue"`, `"green"`, `"cyan"`, `"red"`, `"purple"`,
@@ -197,6 +200,10 @@ pub(crate) fn action_to_map(action: &Action) -> rhai::Map {
m.insert("type".into(), ds("AddGems"));
m.insert("n".into(), Dynamic::from(*n));
}
Action::AlterHealth(dh) => {
m.insert("type".into(), ds("AlterHealth"));
m.insert("dh".into(), Dynamic::from(*dh));
}
Action::SetKey(color, present) => {
m.insert("type".into(), ds("SetKey"));
m.insert("color".into(), Dynamic::from(color.clone()));
+4
View File
@@ -103,6 +103,10 @@ builtins! {
behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true },
script: include_str!("scripts/gem.rhai"),
},
Heart => ["heart" => g(3, 0xCC, 0x22, 0x22)] {
behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true },
script: include_str!("scripts/heart.rhai"),
},
Pusher => [
"pusher_north" => g(30, 0xAA, 0xAA, 0xAA),
"pusher_south" => g(31, 0xAA, 0xAA, 0xAA),
+1 -1
View File
@@ -17,7 +17,7 @@
/// space since a literal NUL is not displayable.
const CP437: [char; 256] = [
// 0x000x0F
' ', '☺', '☻', '', '♦', '♣', '♠', '•', '◘', '○', '◙', '♂', '♀', '♪', '♫', '☼',
' ', '☺', '☻', '', '♦', '♣', '♠', '•', '◘', '○', '◙', '♂', '♀', '♪', '♫', '☼',
// 0x100x1F
'►', '◄', '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '→', '←', '∟', '↔', '▲', '▼',
// 0x200x2F (ASCII space onward)
+17 -6
View File
@@ -152,12 +152,14 @@ pub struct GameState {
/// 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. Players start with `5`. There is no runtime
/// mutation path yet — front-ends only display it.
/// 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` and is
/// display-only for now.
/// [`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,
@@ -188,6 +190,7 @@ impl GameState {
active_scroll: None,
board_transition: None,
player_health: 5,
max_health: 5,
player_gems: 0,
player_keys: Keyring::default(),
};
@@ -298,9 +301,10 @@ impl GameState {
// below also borrows `self`.
let mut logs: Vec<LogLine> = Vec::new();
let mut bumps: Vec<(ObjectId, i64)> = Vec::new();
// Net change to the player's gem count from AddGems actions; applied to
// `self` after the board borrow drops.
// Net change to player stats from AddGems / AlterHealth actions; applied
// to `self` after the board borrow drops.
let mut gem_delta: i64 = 0;
let mut health_delta: i64 = 0;
let mut key_changes: Vec<(String, bool)> = Vec::new();
let mut sends: Vec<(ObjectId, String, Option<ScriptArg>)> = Vec::new();
let mut new_bubbles: Vec<SpeechBubble> = Vec::new();
@@ -403,6 +407,8 @@ impl GameState {
}
// Accumulated and applied to `self.player_gems` after the borrow drops.
Action::AddGems(n) => gem_delta += n,
// Accumulated and applied to `self.player_health` after the borrow drops.
Action::AlterHealth(dh) => health_delta += dh,
// Collected and applied to `self.player_keys` after the borrow drops.
Action::SetKey(color, present) => key_changes.push((color, present)),
// A grab thing despawns itself from its grab() hook.
@@ -426,6 +432,11 @@ impl GameState {
if gem_delta != 0 {
self.player_gems = (self.player_gems as i64 + gem_delta).max(0) as u32;
}
// 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;
}
for (color, present) in key_changes {
if !self.player_keys.set_by_name(&color, present) {
self.log.push(LogLine::error(format!("set_key: unknown color {color:?}")));
+7 -1
View File
@@ -59,7 +59,7 @@
//! `move(dir)`, `delay(secs)`, `now()`, `set_tile(n)`, `log(msg)`, `say(msg)`,
//! `set_fg(fg)`, `set_bg(bg)`, `set_color(fg, bg)`, `set_tag(target, tag, present)`,
//! `send(target_id, fn_name [, arg])`, `teleport(x, y)`, `push(x, y, dir)`,
//! `swap([[src_x, src_y, dst_x, dst_y], …])`, `add_gems(n)`, `set_key(color, present)`, `die()`
//! `swap([[src_x, src_y, dst_x, dst_y], …])`, `add_gems(n)`, `alter_health(dh)`, `set_key(color, present)`, `die()`
//!
//! ## Queue API
//!
@@ -906,6 +906,12 @@ fn register_write_api(engine: &mut Engine, queues: &QueueMap) {
emit(&q, source_of(&ctx), Action::AddGems(n));
});
// alter_health(dh): change the player's health by dh (clamped to [0, max_health]).
let q = queues.clone();
engine.register_fn("alter_health", move |ctx: NativeCallContext, dh: i64| {
emit(&q, source_of(&ctx), Action::AlterHealth(dh));
});
// set_key(color, present): give (true) or take (false) the named key color.
let q = queues.clone();
engine.register_fn(
+8
View File
@@ -0,0 +1,8 @@
// Built-in script for the `heart` archetype.
//
// A heart is a grabbable collectible: walking onto it fires `grab()` instead
// of blocking. It restores 1 health and removes itself from the board.
fn grab() {
alter_health(1);
die();
}