This commit is contained in:
2026-08-10 22:13:21 -05:00
parent 30e09a40c4
commit a8444b25ad
11 changed files with 330 additions and 155 deletions
+5 -13
View File
@@ -176,20 +176,12 @@ pub fn apply_teleport(board: &mut Board, target: i64, x: i64, y: i64) -> Result<
None
};
if let Some(from) = from {
if from.0 != x || from.1 != y {
// Check if we're blocked:
if board.get(x, y).is_none() {
// Not blocked, move it
let thing = board.get_mut(from.0, from.1).take();
*board.get_mut(x, y) = thing;
Ok(())
} else {
Err(format!("teleport({target},{x},{y}): destination is solid"))
}
} else {
// We're teleporting to the same place, which is fine I guess, but no effect:
Ok(())
// Check if we're blocked:
if (from.0 != x || from.1 != y) && board.get(x, y).is_none() {
// Not blocked, move it
board.move_cell(from.into(), (x, y).into());
}
Ok(())
} else {
Err(format!("teleport({target},{x},{y}): no such object"))
}
+25 -1
View File
@@ -2,7 +2,7 @@ use crate::floor::Floor;
use crate::fov::{color_to_rgb, FovCaster, Lighting};
use crate::glyph::Glyph;
use crate::log::LogLine;
use crate::utils::Direction;
use crate::utils::{Direction, Point};
use crate::utils::{ObjectId, RegistryValue};
use std::collections::{HashMap, HashSet};
use crate::portal::Portal;
@@ -619,6 +619,30 @@ impl Board {
}
}
}
/// Moves whatever is in `from` to `to`, leaving an empty cell behind. Silent no-op if either
/// `from` or `to` is out of bounds, or if they're the same cell.
pub fn move_cell(&mut self, from: Point, to: Point) {
if from != to && self.in_bounds(from.into()) && self.in_bounds(to.into()) {
let thing = self.get_mut(from.x as usize, from.y as usize).take();
self.grid[to.x as usize + to.y as usize * self.width] = thing;
}
}
/// Return whether the given point is empty
pub fn is_empty(&self, p: Point) -> bool {
self.get(p.x as usize, p.y as usize).is_none()
}
/// Return whether the given point contains the player
pub fn is_player(&self, p: Point) -> bool {
matches!(self.get(p.x as usize, p.y as usize), Some(Tile::Player))
}
/// Return whether the given point contains an object
pub fn is_object(&self, p: Point) -> bool {
matches!(self.get(p.x as usize, p.y as usize), Some(Tile::Object(_)))
}
}
#[cfg(test)]
+7 -5
View File
@@ -112,12 +112,12 @@ const fn g(tile: char, r: u8, gr: u8, b: u8) -> Glyph {
builtins! {
Gem => ["gem" => g('♦', 0x50, 0x50, 0xFF)] {
enter: EnterResponse::Grab,
enter: EnterResponse::Block,
optics: Optics { opaque: false, glow: 0 },
script: ScriptKey::Builtin("gem"),
},
Heart => ["heart" => g('♡', 0xCC, 0x22, 0x22)] {
enter: EnterResponse::Grab,
enter: EnterResponse::Block,
optics: Optics { opaque: false, glow: 0 },
script: ScriptKey::Builtin("heart"),
},
@@ -147,7 +147,7 @@ builtins! {
"transporter_east" => g(')', 0x55, 0xFF, 0xFF), // ')'
"transporter_west" => g('(', 0x55, 0xFF, 0xFF), // '('
] {
enter: EnterResponse::Hook,
enter: EnterResponse::Block,
optics: Optics { opaque: false, glow: 0 },
script: ScriptKey::Builtin("transporter"),
},
@@ -174,9 +174,10 @@ builtins! {
script: ScriptKey::None
},
Crate => ["crate" => g('■', 0xaa, 0xaa, 0xaa)] { // CP437 ■ (small filled square)
enter: EnterResponse::Push(Pushable::Any),
//enter: EnterResponse::Push(Pushable::Any),
enter: EnterResponse::Block,
optics: Optics { opaque: true, glow: 0 },
script: ScriptKey::None
script: ScriptKey::Builtin("crate"),
},
HCrate => ["hcrate" => g('↔', 0xaa, 0xaa, 0xaa)] { // CP437 ↔ (left-right arrow) — pushable east/west
enter: EnterResponse::Push(Pushable::Horizontal),
@@ -199,6 +200,7 @@ lazy_static! {
m.insert(ScriptKey::Builtin("spinner"), include_str!("scripts/spinner.rhai"));
m.insert(ScriptKey::Builtin("transporter"), include_str!("scripts/transporter.rhai"));
m.insert(ScriptKey::Builtin("key"), include_str!("scripts/key.rhai"));
m.insert(ScriptKey::Builtin("crate"), include_str!("scripts/crate.rhai"));
m
};
}
+95 -75
View File
@@ -2,10 +2,11 @@ use crate::action::{apply_push, apply_shift, apply_teleport, Action, BoardAction
use crate::board::Board;
use crate::log::LogLine;
use crate::script::ScriptHost;
use crate::utils::{Direction, ObjectId};
use crate::utils::{Direction, ObjectId, Point, Pushable};
use crate::world::World;
use std::cell::{Ref, RefMut};
use std::collections::{BTreeSet, HashSet, VecDeque};
use std::fmt::format;
use std::hash::Hash;
use std::time::Duration;
@@ -295,7 +296,8 @@ impl GameState {
apply_teleport(&mut self.board_mut(), target, x, y).unwrap_or_else(|e| log_sink.error(e))
}
Action::Push { x, y, dir } => {
apply_push(&mut self.board_mut(), x, y, dir).unwrap_or_else(|e| log_sink.error(e))
self.resolve_move((x as usize, y as usize), dir);
//apply_push(&mut self.board_mut(), x, y, dir).unwrap_or_else(|e| log_sink.error(e))
}
Action::Shift(cells) => {
apply_shift(&mut self.board_mut(), &cells).unwrap_or_else(|e| log_sink.error(e))
@@ -385,6 +387,96 @@ impl GameState {
self.run_init();
}
fn resolve_move(&mut self, from: (usize, usize), dir: Direction) -> bool {
// Get the target coords, if they're out of bounds then the move fails.
let target: Point = dir.from_point(from.0 as i64, from.1 as i64).into();
if !self.board().in_bounds((target.x, target.y)) { return false; }
if self.board().is_empty(target) {
// If it's empty, we can move in there; do so and return true:
self.board_mut().move_cell(from.into(), target);
} else if self.board().is_player(target) {
// Target cell contains player, who's pushable, so we'll recurse and see what happens:
if self.resolve_move((target.x as usize, target.y as usize), dir) {
self.board_mut().move_cell(from.into(), target);
}
} else {
// Otherwise, what enter response does the thing there have? We need to be able to call
// hooks on objects, so we can't hold a mut reference to the board going into this match
let id = {
if let Some(Tile::Object(b)) = self.board_mut().get(target.x as usize, target.y as usize) && let def = b.as_ref() {
def.scripting.id
} else {
unreachable!("moving into the player was handled above")
}
};
// Whether the player is what initiated the move directly. Matters for grab / swap
// let from_player = matches!(self.board().get(from.0, from.1), Some(Tile::Player));
// Call the target's bump hook and resolve whatever it did
let actions = self.scripts.run_bump(id, dir.opposite());
self.apply_actions(actions);
// First do things to try and call any hooks relevant:
// let actions = match resp {
// // We block all moves, return false
// EnterResponse::Block => { self.scripts.run_bump(id, dir.opposite()) }
//
// // The player can grab things directly, so let's do that
// EnterResponse::Grab if from_player => {
// let a = self.scripts.run_grab(id);
// self.board_mut().get_mut(target.0, target.1).take();
// a
// }
//
// // Grabbables not from the player act as pushable, we need to recurse
// EnterResponse::Grab => { self.resolve_move(target, dir); vec![] }
//
// // Pushable if we're allowed to push that way recurses
// EnterResponse::Push(p) if p.allows(dir) => { self.resolve_move(target, dir); vec![] }
//
// // Pushable in a disallowed direction blocks
// EnterResponse::Push(p) => { self.scripts.run_bump(id, dir.opposite()) }
//
// // Swaps let the player swap places, but we should return false afterward because we
// // haven't left the source cell empty. In practice this won't matter because right
// // now we never recurse _into_ a player-source-cell, all moves are initiated by the
// // player, but still.
// EnterResponse::Swap if from_player => {
// let mut board = self.board_mut();
// let mover = board.get_mut(from.0, from.1).take();
// let swapper = board.get_mut(target.0, target.1).take();
// board.get_mut(target.0, target.1).replace(mover.unwrap());
// board.get_mut(from.0, from.1).replace(swapper.unwrap());
// vec![]
// }
//
// // Swaps in a chain are just normal pushable, recurse:
// EnterResponse::Swap => { self.resolve_move(target, dir); vec![] }
//
// // Squish just lets the mover overwrite, and always leaves the source cell empty:
// EnterResponse::Squish => { self.board_mut().get_mut(from.0, from.1).take(); vec![] }
//
// // Finally, hook, we need to call a hook and let it do its thing:
// EnterResponse::Hook => { self.scripts.run_bump(id, dir.opposite()) }
// };
// If there were actions performed by the hooks, do them.
// self.apply_actions(actions);
// Now, check again if the target cell is empty. If it is, put the mover there. Also
// check that the source cell still contains something! It may have been teleported away.
if !self.board().is_empty(from.into()) && self.board().is_empty(target) {
self.board_mut().move_cell(from.into(), target);
}
}
// Either way, return whether the source cell is now empty, so calls up the chain
// can move into it (push chains)
self.board().is_empty(from.into())
}
/// Attempts to move the player one cell in `dir`.
///
/// The move is ignored if the target cell is out of bounds, or it is neither
@@ -399,80 +491,8 @@ impl GameState {
if !self.board().in_bounds(target) {
return;
}
let (nx, ny) = (target.0 as usize, target.1 as usize);
// We need to be able to call hooks on objects, so we can't hold a mut reference to the
// board going into this match
let obj_data = {
if let Some(Tile::Object(b)) = self.board_mut().get(nx, ny) && let def = b.as_ref() {
Some((def.scripting.id, def.enter_response))
} else {
None
}
};
if let Some((id, enter_response)) = obj_data {
let actions = match enter_response {
EnterResponse::Block => {
// Call the bump hook
self.scripts.run_bump(id, dir.opposite())
}
EnterResponse::Grab => {
// Call the hook to get the actions and then stamp the player on top
let a = self.scripts.run_grab(id);
{
let mut board = self.board_mut();
board.get_mut(player_loc.0, player_loc.1).take();
board.get_mut(nx, ny).replace(Tile::Player);
}
a
}
EnterResponse::Push(pushable) => {
// First, can we push?
if self.board().can_push(nx, ny, dir) {
// The player is pushable, so, this amounts to the same thing and saves a
// couple replace()s
self.board_mut().push(player_loc.0, player_loc.1, dir);
vec![] // There's no push hook, just pushing doesn't call scripts
} else {
// This is actually not pushable in this way, so we're gonna bump instead:
self.scripts.run_bump(id, dir.opposite())
}
}
EnterResponse::Hook => {
vec![] // TODO this hook needs to exist and work. It's documented in tile.rb. Has implications for push as well
// plan: get rid of can_push in board. Make a board::pushes_into_hook or something, find the hook-enter
// object at the end of this chain. Trying to push calls that, if there's no hook object then it pushes, if
// that returns false then it bumps. If there is a hook object, call the hook, run the actions. If it _leaves
// the cell empty,_ then call push. Otherwise bump.
// Maybe have a pushresult enum or something that board::push returns, "hook(id, bumpid)" or "bump(id)" or "moved".
// If the situation is: `@b++h` then moving to the east, the bumpid would be b (the thing you actually touched),
// hook id would be h (the thing with the hook enterresponse). Board can identify object chains but not actually
// call hooks.
}
EnterResponse::Swap => {
let mut board = self.board_mut();
// Swap the two
let tgt = board.get_mut(nx, ny).replace(Tile::Player);
*board.get_mut(player_loc.0, player_loc.1) = tgt;
vec![]
}
EnterResponse::Squish => {
let mut board = self.board_mut();
// Stamp over it, squishing it
board.get_mut(player_loc.0, player_loc.1).take();
board.get_mut(nx, ny).replace(Tile::Player);
vec![]
}
};
self.apply_actions(actions);
} else {
// There's not an object there, we can just move the player
let mut board = self.board_mut();
board.get_mut(player_loc.0, player_loc.1).take();
board.get_mut(nx, ny).replace(Tile::Player);
}
self.resolve_move(player_loc, dir);
// Check if we actually moved
let new_loc = self.board().player_pos();
+3 -2
View File
@@ -37,7 +37,7 @@ use crate::action::{Action, BoardAction, ScrollLine, SendArg, MOVE_COST};
use crate::game::SAY_DURATION;
use crate::log::LogLine;
use crate::object_def::ObjectDef;
use crate::utils::{Direction, LogSink, Hook, ObjectId};
use crate::utils::{Direction, LogSink, Hook, ObjectId, Point};
use rhai::{
Array, CallFnOptions, Dynamic, Engine, ImmutableString, Module, NativeCallContext,
Scope, AST,
@@ -48,7 +48,6 @@ use crate::api::object_info::ObjectInfo;
use crate::api::player::PlayerWithPos;
use crate::api::queue::ObjQueue;
use crate::api::registry::Registry;
use crate::builtin::BUILTIN_SOURCES;
use color::Rgba8;
use crate::glyph::parse_color;
use crate::glyph::Glyph;
@@ -116,6 +115,8 @@ impl ScriptHost {
Glyph::register(&mut engine, log_sink.clone());
ObjQueue::register(&mut engine, log_sink.clone());
Registry::register(&mut engine, log_sink.clone());
Direction::register(&mut engine, log_sink.clone());
Point::register(&mut engine, log_sink.clone());
register_write_api(&mut engine, board_ref.clone(), log_sink.clone());
register_global_constants(&mut engine, board_ref.clone(), player.clone());
+8
View File
@@ -0,0 +1,8 @@
// Built-in script for the `crate` archetype (see kiln-core/src/builtin_scripts.rs).
fn bump(me, dir) {
push(me.x, me.y, dir.opposite); // Try to clear our target cell
let tgt = dir.opposite.from_point(me.x, me.y);
// This will silently-nop if tgt is occupied, and it will evaluate that after the push
// (queue both right now, evaluate in that order after)
teleport(me.id, tgt.x, tgt.y);
}
+1 -1
View File
@@ -3,7 +3,7 @@
// A gem is a grabbable collectible: walking onto it (or pushing it into the
// player) fires this `grab()` hook instead of blocking. We bump the player's gem
// count and remove ourselves from the board.
fn grab(me) {
fn bump(me, _dir) {
alter_gems(1);
die();
}
+1 -1
View File
@@ -2,7 +2,7 @@
//
// 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(me) {
fn bump(me, _dir) {
alter_health(1);
die();
}
+1 -1
View File
@@ -3,7 +3,7 @@
// A gem is a grabbable collectible: walking onto it (or pushing it into the
// player) fires this `grab()` hook instead of blocking. We bump the player's gem
// count and remove ourselves from the board.
fn grab(me) {
fn bump(me, _dir) {
let colors = [
"red",
"orange",
+44 -2
View File
@@ -1,9 +1,11 @@
use std::cell::RefCell;
use std::fmt::Display;
use std::rc::Rc;
use rhai::Dynamic;
use rhai::{Dynamic, Engine};
use serde::{Deserialize, Serialize};
use crate::keys::Keyring;
use crate::log::LogLine;
use crate::script::Registerable;
/// Which directions a solid may be pushed in.
///
@@ -121,10 +123,50 @@ impl Direction {
/// Translate the given point in this direction
pub fn from_point(self, x: i64, y: i64) -> (i64, i64) {
(x as i64 + self.dx(), y as i64 + self.dy())
(x + self.dx(), y + self.dy())
}
}
impl Registerable for Direction {
fn register(engine: &mut Engine, _log_sink: LogSink) {
engine.register_type_with_name::<Direction>("Direction")
.register_get("opposite", |dir: &mut Direction| dir.opposite())
.register_fn("from_point", |dir: &mut Direction, x: i64, y: i64| Point::from(dir.from_point(x, y)));
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Point {
pub x: i64,
pub y: i64,
}
impl From<(i64, i64)> for Point {
fn from((x, y): (i64, i64)) -> Self { Self { x, y } }
}
impl From<(usize, usize)> for Point {
fn from((x, y): (usize, usize)) -> Self { Self { x: x as i64, y: y as i64 } }
}
impl Into<(i64, i64)> for Point {
fn into(self) -> (i64, i64) { (self.x, self.y) }
}
impl Display for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl Registerable for Point {
fn register(engine: &mut Engine, _log_sink: LogSink) {
engine.register_type_with_name::<Point>("Point")
.register_get("x", |p: &mut Point| p.x)
.register_get("y", |p: &mut Point| p.y);
engine.register_fn("xy", |x: i64, y: i64| Point::from((x, y)));
}
}
/// A value that can be stored in a board's script registry across board transitions.
///
/// Restricted to primitive types that convert cleanly to and from `rhai::Dynamic`