wip 1
This commit is contained in:
+2
-10
@@ -176,20 +176,12 @@ pub fn apply_teleport(board: &mut Board, target: i64, x: i64, y: i64) -> Result<
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
if let Some(from) = from {
|
if let Some(from) = from {
|
||||||
if from.0 != x || from.1 != y {
|
|
||||||
// Check if we're blocked:
|
// Check if we're blocked:
|
||||||
if board.get(x, y).is_none() {
|
if (from.0 != x || from.1 != y) && board.get(x, y).is_none() {
|
||||||
// Not blocked, move it
|
// Not blocked, move it
|
||||||
let thing = board.get_mut(from.0, from.1).take();
|
board.move_cell(from.into(), (x, y).into());
|
||||||
*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(())
|
Ok(())
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Err(format!("teleport({target},{x},{y}): no such object"))
|
Err(format!("teleport({target},{x},{y}): no such object"))
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-1
@@ -2,7 +2,7 @@ use crate::floor::Floor;
|
|||||||
use crate::fov::{color_to_rgb, FovCaster, Lighting};
|
use crate::fov::{color_to_rgb, FovCaster, Lighting};
|
||||||
use crate::glyph::Glyph;
|
use crate::glyph::Glyph;
|
||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
use crate::utils::Direction;
|
use crate::utils::{Direction, Point};
|
||||||
use crate::utils::{ObjectId, RegistryValue};
|
use crate::utils::{ObjectId, RegistryValue};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use crate::portal::Portal;
|
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)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -112,12 +112,12 @@ const fn g(tile: char, r: u8, gr: u8, b: u8) -> Glyph {
|
|||||||
|
|
||||||
builtins! {
|
builtins! {
|
||||||
Gem => ["gem" => g('♦', 0x50, 0x50, 0xFF)] {
|
Gem => ["gem" => g('♦', 0x50, 0x50, 0xFF)] {
|
||||||
enter: EnterResponse::Grab,
|
enter: EnterResponse::Block,
|
||||||
optics: Optics { opaque: false, glow: 0 },
|
optics: Optics { opaque: false, glow: 0 },
|
||||||
script: ScriptKey::Builtin("gem"),
|
script: ScriptKey::Builtin("gem"),
|
||||||
},
|
},
|
||||||
Heart => ["heart" => g('♡', 0xCC, 0x22, 0x22)] {
|
Heart => ["heart" => g('♡', 0xCC, 0x22, 0x22)] {
|
||||||
enter: EnterResponse::Grab,
|
enter: EnterResponse::Block,
|
||||||
optics: Optics { opaque: false, glow: 0 },
|
optics: Optics { opaque: false, glow: 0 },
|
||||||
script: ScriptKey::Builtin("heart"),
|
script: ScriptKey::Builtin("heart"),
|
||||||
},
|
},
|
||||||
@@ -147,7 +147,7 @@ builtins! {
|
|||||||
"transporter_east" => g(')', 0x55, 0xFF, 0xFF), // ')'
|
"transporter_east" => g(')', 0x55, 0xFF, 0xFF), // ')'
|
||||||
"transporter_west" => g('(', 0x55, 0xFF, 0xFF), // '('
|
"transporter_west" => g('(', 0x55, 0xFF, 0xFF), // '('
|
||||||
] {
|
] {
|
||||||
enter: EnterResponse::Hook,
|
enter: EnterResponse::Block,
|
||||||
optics: Optics { opaque: false, glow: 0 },
|
optics: Optics { opaque: false, glow: 0 },
|
||||||
script: ScriptKey::Builtin("transporter"),
|
script: ScriptKey::Builtin("transporter"),
|
||||||
},
|
},
|
||||||
@@ -174,9 +174,10 @@ builtins! {
|
|||||||
script: ScriptKey::None
|
script: ScriptKey::None
|
||||||
},
|
},
|
||||||
Crate => ["crate" => g('■', 0xaa, 0xaa, 0xaa)] { // CP437 ■ (small filled square)
|
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 },
|
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
|
HCrate => ["hcrate" => g('↔', 0xaa, 0xaa, 0xaa)] { // CP437 ↔ (left-right arrow) — pushable east/west
|
||||||
enter: EnterResponse::Push(Pushable::Horizontal),
|
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("spinner"), include_str!("scripts/spinner.rhai"));
|
||||||
m.insert(ScriptKey::Builtin("transporter"), include_str!("scripts/transporter.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("key"), include_str!("scripts/key.rhai"));
|
||||||
|
m.insert(ScriptKey::Builtin("crate"), include_str!("scripts/crate.rhai"));
|
||||||
m
|
m
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+95
-75
@@ -2,10 +2,11 @@ use crate::action::{apply_push, apply_shift, apply_teleport, Action, BoardAction
|
|||||||
use crate::board::Board;
|
use crate::board::Board;
|
||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
use crate::script::ScriptHost;
|
use crate::script::ScriptHost;
|
||||||
use crate::utils::{Direction, ObjectId};
|
use crate::utils::{Direction, ObjectId, Point, Pushable};
|
||||||
use crate::world::World;
|
use crate::world::World;
|
||||||
use std::cell::{Ref, RefMut};
|
use std::cell::{Ref, RefMut};
|
||||||
use std::collections::{BTreeSet, HashSet, VecDeque};
|
use std::collections::{BTreeSet, HashSet, VecDeque};
|
||||||
|
use std::fmt::format;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::time::Duration;
|
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))
|
apply_teleport(&mut self.board_mut(), target, x, y).unwrap_or_else(|e| log_sink.error(e))
|
||||||
}
|
}
|
||||||
Action::Push { x, y, dir } => {
|
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) => {
|
Action::Shift(cells) => {
|
||||||
apply_shift(&mut self.board_mut(), &cells).unwrap_or_else(|e| log_sink.error(e))
|
apply_shift(&mut self.board_mut(), &cells).unwrap_or_else(|e| log_sink.error(e))
|
||||||
@@ -385,6 +387,96 @@ impl GameState {
|
|||||||
self.run_init();
|
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`.
|
/// 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
|
/// 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) {
|
if !self.board().in_bounds(target) {
|
||||||
return;
|
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
|
self.resolve_move(player_loc, dir);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we actually moved
|
// Check if we actually moved
|
||||||
let new_loc = self.board().player_pos();
|
let new_loc = self.board().player_pos();
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ use crate::action::{Action, BoardAction, ScrollLine, SendArg, MOVE_COST};
|
|||||||
use crate::game::SAY_DURATION;
|
use crate::game::SAY_DURATION;
|
||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
use crate::object_def::ObjectDef;
|
use crate::object_def::ObjectDef;
|
||||||
use crate::utils::{Direction, LogSink, Hook, ObjectId};
|
use crate::utils::{Direction, LogSink, Hook, ObjectId, Point};
|
||||||
use rhai::{
|
use rhai::{
|
||||||
Array, CallFnOptions, Dynamic, Engine, ImmutableString, Module, NativeCallContext,
|
Array, CallFnOptions, Dynamic, Engine, ImmutableString, Module, NativeCallContext,
|
||||||
Scope, AST,
|
Scope, AST,
|
||||||
@@ -48,7 +48,6 @@ use crate::api::object_info::ObjectInfo;
|
|||||||
use crate::api::player::PlayerWithPos;
|
use crate::api::player::PlayerWithPos;
|
||||||
use crate::api::queue::ObjQueue;
|
use crate::api::queue::ObjQueue;
|
||||||
use crate::api::registry::Registry;
|
use crate::api::registry::Registry;
|
||||||
use crate::builtin::BUILTIN_SOURCES;
|
|
||||||
use color::Rgba8;
|
use color::Rgba8;
|
||||||
use crate::glyph::parse_color;
|
use crate::glyph::parse_color;
|
||||||
use crate::glyph::Glyph;
|
use crate::glyph::Glyph;
|
||||||
@@ -116,6 +115,8 @@ impl ScriptHost {
|
|||||||
Glyph::register(&mut engine, log_sink.clone());
|
Glyph::register(&mut engine, log_sink.clone());
|
||||||
ObjQueue::register(&mut engine, log_sink.clone());
|
ObjQueue::register(&mut engine, log_sink.clone());
|
||||||
Registry::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_write_api(&mut engine, board_ref.clone(), log_sink.clone());
|
||||||
register_global_constants(&mut engine, board_ref.clone(), player.clone());
|
register_global_constants(&mut engine, board_ref.clone(), player.clone());
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
// A gem is a grabbable collectible: walking onto it (or pushing it into the
|
// 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
|
// player) fires this `grab()` hook instead of blocking. We bump the player's gem
|
||||||
// count and remove ourselves from the board.
|
// count and remove ourselves from the board.
|
||||||
fn grab(me) {
|
fn bump(me, _dir) {
|
||||||
alter_gems(1);
|
alter_gems(1);
|
||||||
die();
|
die();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// A heart is a grabbable collectible: walking onto it fires `grab()` instead
|
// A heart is a grabbable collectible: walking onto it fires `grab()` instead
|
||||||
// of blocking. It restores 1 health and removes itself from the board.
|
// of blocking. It restores 1 health and removes itself from the board.
|
||||||
fn grab(me) {
|
fn bump(me, _dir) {
|
||||||
alter_health(1);
|
alter_health(1);
|
||||||
die();
|
die();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// A gem is a grabbable collectible: walking onto it (or pushing it into the
|
// 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
|
// player) fires this `grab()` hook instead of blocking. We bump the player's gem
|
||||||
// count and remove ourselves from the board.
|
// count and remove ourselves from the board.
|
||||||
fn grab(me) {
|
fn bump(me, _dir) {
|
||||||
let colors = [
|
let colors = [
|
||||||
"red",
|
"red",
|
||||||
"orange",
|
"orange",
|
||||||
|
|||||||
+44
-2
@@ -1,9 +1,11 @@
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use rhai::Dynamic;
|
use rhai::{Dynamic, Engine};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::keys::Keyring;
|
||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
|
use crate::script::Registerable;
|
||||||
|
|
||||||
/// Which directions a solid may be pushed in.
|
/// Which directions a solid may be pushed in.
|
||||||
///
|
///
|
||||||
@@ -121,10 +123,50 @@ impl Direction {
|
|||||||
|
|
||||||
/// Translate the given point in this direction
|
/// Translate the given point in this direction
|
||||||
pub fn from_point(self, x: i64, y: i64) -> (i64, i64) {
|
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.
|
/// 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`
|
/// Restricted to primitive types that convert cleanly to and from `rhai::Dynamic`
|
||||||
|
|||||||
+140
-54
@@ -8,7 +8,7 @@ start = "start"
|
|||||||
greeter = """
|
greeter = """
|
||||||
fn init(me) {
|
fn init(me) {
|
||||||
log(`hello from object — player at ${Player.x}, ${Player.y}`);
|
log(`hello from object — player at ${Player.x}, ${Player.y}`);
|
||||||
set_tile(2); // change my glyph to ☻ — proves the write path
|
set_tile("☻"); // change my glyph — proves the write path
|
||||||
say("Hello there,\\ntraveller!");
|
say("Hello there,\\ntraveller!");
|
||||||
log(`Player health: ${Player.health}`);
|
log(`Player health: ${Player.health}`);
|
||||||
}
|
}
|
||||||
@@ -37,15 +37,15 @@ fn init(me) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// move() costs 250 ms, so the object steps ~4 cells/sec no matter how often
|
// move() costs 250 ms, so the object steps ~4 cells/sec no matter how often
|
||||||
// tick() runs. Queue.length() avoids piling up moves; blocked() avoids walking
|
// tick() runs. The engine only calls tick() once this object's queue has drained,
|
||||||
// into a wall (or another object's already-queued move this tick).
|
// so a whole dance can be queued up front without piling up.
|
||||||
fn tick(me, dt) {
|
fn tick(me, dt) {
|
||||||
move(East);
|
move(East);
|
||||||
move(South);
|
move(South);
|
||||||
move(North);
|
move(North);
|
||||||
move(East);
|
move(East);
|
||||||
say("Hey!", 0.5);
|
say("Hey!", 0.5);
|
||||||
delay(me, 0.5);
|
me.delay(0.5);
|
||||||
move(West);
|
move(West);
|
||||||
move(South);
|
move(South);
|
||||||
move(North);
|
move(North);
|
||||||
@@ -159,7 +159,7 @@ fn bump(me, _dir) {
|
|||||||
yammerer = """
|
yammerer = """
|
||||||
fn tick(me, dt) {
|
fn tick(me, dt) {
|
||||||
say("blahblahblah", 2.0);
|
say("blahblahblah", 2.0);
|
||||||
delay(2);
|
delay(2.0);
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -175,8 +175,8 @@ fn tick(me, dt) {
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# A trigger script: no glyph, not solid — it just watches for the player stepping
|
# A sensor script: no glyph, off the grid — it just watches for the player stepping
|
||||||
# onto its cell and logs. Triggers are placed via [[boards.NAME.triggers]].
|
# onto its cell and logs. Sensors are placed via [[boards.NAME.sensors]].
|
||||||
tripwire = """
|
tripwire = """
|
||||||
fn enter(me, dir) {
|
fn enter(me, dir) {
|
||||||
if Player.x == me.x && Player.y == me.y {
|
if Player.x == me.x && Player.y == me.y {
|
||||||
@@ -185,7 +185,7 @@ fn enter(me, dir) {
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[boards.start.map]
|
[boards.start]
|
||||||
name = "Starting Room"
|
name = "Starting Room"
|
||||||
width = 60
|
width = 60
|
||||||
height = 25
|
height = 25
|
||||||
@@ -193,23 +193,24 @@ height = 25
|
|||||||
# grass/dirt/stone/water floor is not representable with one floor attribute).
|
# grass/dirt/stone/water floor is not representable with one floor attribute).
|
||||||
floor = { biome = "grass" }
|
floor = { biome = "grass" }
|
||||||
|
|
||||||
# The single grid: all solids and most non-solids (terrain, the player, objects and
|
# The grid holds every solid: terrain, crates, the player and scripted objects. A
|
||||||
# the portal). A space is always a transparent empty cell, so the floor shows through.
|
# space is a transparent empty cell, so the floor shows through. Portals and sensors
|
||||||
[boards.start.grid]
|
# are *not* here — they sit alongside the grid (see the arrays further down), because
|
||||||
content = """
|
# the player has to be able to share their cell.
|
||||||
|
grid = """
|
||||||
############################################################
|
############################################################
|
||||||
# #
|
# #
|
||||||
# o #
|
# o #
|
||||||
# oS #
|
# oS #
|
||||||
# #
|
# #
|
||||||
# 1 #
|
# #
|
||||||
# )######( B #
|
# )######( B #
|
||||||
# # #
|
# # #
|
||||||
# +++ # v #
|
# # v #
|
||||||
# # #
|
# # #
|
||||||
# | #
|
# | #
|
||||||
# #
|
# #
|
||||||
# V oo G - @ #
|
# V oo - @ #
|
||||||
# /# #
|
# /# #
|
||||||
# t #
|
# t #
|
||||||
# #
|
# #
|
||||||
@@ -223,52 +224,99 @@ content = """
|
|||||||
# #
|
# #
|
||||||
############################################################
|
############################################################
|
||||||
"""
|
"""
|
||||||
[boards.start.grid.palette]
|
[boards.start.palette]
|
||||||
"#" = { kind = "wall", tile = "#", fg = "#808080", bg = "#606060" }
|
"#" = { type = "builtin", kind = "wall", tile = "#", fg = "#808080", bg = "#606060" }
|
||||||
"+" = { kind = "banana", tile = "#", fg = "#808080", bg = "#606060" } # unknown kind -> ErrorBlock demo
|
"o" = { type = "builtin", kind = "crate", tile = 254, fg = "#aaaaaa", bg = "#000000" }
|
||||||
"o" = { kind = "crate", tile = 254, fg = "#aaaaaa", bg = "#000000" }
|
"-" = { type = "builtin", kind = "hcrate", tile = 29, fg = "#aaaaaa", bg = "#000000" }
|
||||||
"-" = { kind = "hcrate", tile = 29, fg = "#aaaaaa", bg = "#000000" }
|
"|" = { type = "builtin", kind = "vcrate", tile = 18, fg = "#aaaaaa", bg = "#000000" }
|
||||||
"|" = { kind = "vcrate", tile = 18, fg = "#aaaaaa", bg = "#000000" }
|
">" = { type = "builtin", kind = "pusher_east" }
|
||||||
">" = { kind = "pusher_east" }
|
"^" = { type = "builtin", kind = "pusher_north" }
|
||||||
"^" = { kind = "pusher_north" }
|
"/" = { type = "builtin", kind = "spinner_cw" }
|
||||||
"@" = { kind = "player" }
|
"t" = { type = "builtin", kind = "gem" }
|
||||||
"1" = { kind = "portal", name = "to_house", target_map = "house", target_entry = "from_start" }
|
"v" = { type = "builtin", kind = "transporter_south" }
|
||||||
"G" = { kind = "object", tile = "#", fg = "#aa3333", bg = "#000000", solid = false, script_name = "greeter" }
|
")" = { type = "builtin", kind = "transporter_east" }
|
||||||
"V" = { kind = "object", tile = 1, fg = "#33aa33", bg = "#000000", script_name = "mover" }
|
"(" = { type = "builtin", kind = "transporter_west" }
|
||||||
"M" = { kind = "object", tile = 15, fg = "#ffdd88", bg = "#000000", solid = true, name = "muffin", script_name = "muffin" }
|
"@" = { type = "player" }
|
||||||
"B" = { kind = "object", tile = 240, fg = "#cc9944", bg = "#000000", solid = true, name = "noticeboard", script_name = "noticeboard" }
|
|
||||||
"S" = { kind = "object", tile = 240, fg = "#cc9944", bg = "#000000", solid = true, name = "shifter", script_name = "shifter" }
|
|
||||||
"/" = { kind = "spinner_cw" }
|
|
||||||
"t" = { kind = "gem" }
|
|
||||||
"v" = { kind = "transporter_south" }
|
|
||||||
")" = { kind = "transporter_east" }
|
|
||||||
"(" = { kind = "transporter_west" }
|
|
||||||
|
|
||||||
# Triggers: invisible, non-solid, script-only objects placed off the grid. This one
|
# The dancer: walks a fixed loop, and teleports back to where it started if the
|
||||||
# watches for the player and logs when they reach its cell.
|
# board Registry remembers a previous position.
|
||||||
[[boards.start.triggers]]
|
[boards.start.palette.V]
|
||||||
|
type = "object"
|
||||||
|
script = "mover"
|
||||||
|
enter = "block"
|
||||||
|
tile = 1
|
||||||
|
fg = "#33aa33"
|
||||||
|
bg = "#000000"
|
||||||
|
|
||||||
|
[boards.start.palette.M]
|
||||||
|
type = "object"
|
||||||
|
script = "muffin"
|
||||||
|
name = "muffin"
|
||||||
|
enter = "block"
|
||||||
|
tile = 15
|
||||||
|
fg = "#ffdd88"
|
||||||
|
bg = "#000000"
|
||||||
|
|
||||||
|
[boards.start.palette.B]
|
||||||
|
type = "object"
|
||||||
|
script = "noticeboard"
|
||||||
|
name = "noticeboard"
|
||||||
|
enter = "block"
|
||||||
|
tile = 240
|
||||||
|
fg = "#cc9944"
|
||||||
|
bg = "#000000"
|
||||||
|
|
||||||
|
[boards.start.palette.S]
|
||||||
|
type = "object"
|
||||||
|
script = "shifter"
|
||||||
|
name = "shifter"
|
||||||
|
enter = "block"
|
||||||
|
tile = 240
|
||||||
|
fg = "#cc9944"
|
||||||
|
bg = "#000000"
|
||||||
|
|
||||||
|
[[boards.start.portals]]
|
||||||
|
x = 2
|
||||||
|
y = 5
|
||||||
|
name = "to_house"
|
||||||
|
target_board = "house"
|
||||||
|
target_name = "from_start"
|
||||||
|
|
||||||
|
# The greeter is a sensor, not a grid object: it is visible but does not block, so
|
||||||
|
# stepping onto it fires its `enter` hook. (A non-solid grid object no longer
|
||||||
|
# exists — every object in the grid is solid.)
|
||||||
|
[[boards.start.sensors]]
|
||||||
|
x = 36
|
||||||
|
y = 12
|
||||||
|
script = "greeter"
|
||||||
|
draw_layer = "Below" # capitalized: DrawLayer's variants are not renamed
|
||||||
|
glyph = { tile = "#", fg = "#aa3333", bg = "#000000" }
|
||||||
|
|
||||||
|
# An invisible tripwire, deliberately placed *under* the hcrate at (40,12): you have
|
||||||
|
# to shove the crate west before you can stand here and set it off.
|
||||||
|
[[boards.start.sensors]]
|
||||||
x = 40
|
x = 40
|
||||||
y = 12
|
y = 12
|
||||||
script_name = "tripwire"
|
script = "tripwire"
|
||||||
|
|
||||||
[boards.house.map]
|
[boards.house]
|
||||||
name = "The House"
|
name = "The House"
|
||||||
width = 60
|
width = 60
|
||||||
height = 25
|
height = 25
|
||||||
dark = true
|
dark = true
|
||||||
|
|
||||||
# A single fixed floor glyph across the whole board.
|
# A single fixed floor glyph across the whole board. There are no light sources
|
||||||
floor = { tile = 176, fg = "#888888", bg = "#444444" }
|
# here, so the board is lit only by the player's own torch (SIGHT_RADIUS, 10).
|
||||||
|
floor = { glyph = { tile = 176, fg = "#888888", bg = "#444444" } }
|
||||||
|
|
||||||
# The single grid: terrain, the player, objects and the return portal.
|
# The grid: terrain, the player and objects. The return portal is off-grid, below.
|
||||||
[boards.house.grid]
|
grid = """
|
||||||
content = """
|
|
||||||
############################################################
|
############################################################
|
||||||
# #
|
# #
|
||||||
# K ###F### #
|
# K ###F### #
|
||||||
# # # #
|
# # # #
|
||||||
# ####### #
|
# ####### #
|
||||||
# 1 #
|
# #
|
||||||
# #
|
# #
|
||||||
# ######### T #
|
# ######### T #
|
||||||
# # # T #
|
# # # T #
|
||||||
@@ -289,12 +337,50 @@ content = """
|
|||||||
# #
|
# #
|
||||||
############################################################
|
############################################################
|
||||||
"""
|
"""
|
||||||
[boards.house.grid.palette]
|
[boards.house.palette]
|
||||||
"#" = { kind = "wall", tile = 178, fg = "#888888", bg = "#555555" }
|
"#" = { type = "builtin", kind = "wall", tile = 178, fg = "#888888", bg = "#555555" }
|
||||||
"@" = { kind = "player" }
|
"@" = { type = "player" }
|
||||||
"1" = { kind = "portal", name = "from_start", target_map = "start", target_entry = "to_house" }
|
|
||||||
"K" = { kind = "object", tile = 240, fg = "#aa7733", bg = "#3d1c00", solid = true, name = "bookshelf", script_name = "bookshelf" }
|
[boards.house.palette.K]
|
||||||
"F" = { kind = "object", tile = 15, fg = "#ff8800", bg = "#220000", solid = true, name = "fireplace", script_name = "fireplace" }
|
type = "object"
|
||||||
"C" = { kind = "object", tile = 240, fg = "#ccaa44", bg = "#222200", solid = true, name = "chest", script_name = "chest" }
|
script = "bookshelf"
|
||||||
"T" = { kind = "object", tile = 1, fg = "#99ff44", bg = "#000000", solid = true, script_name = "yammerer" }
|
name = "bookshelf"
|
||||||
|
enter = "block"
|
||||||
|
tile = 240
|
||||||
|
fg = "#aa7733"
|
||||||
|
bg = "#3d1c00"
|
||||||
|
|
||||||
|
[boards.house.palette.F]
|
||||||
|
type = "object"
|
||||||
|
script = "fireplace"
|
||||||
|
name = "fireplace"
|
||||||
|
enter = "block"
|
||||||
|
tile = 15
|
||||||
|
fg = "#ff8800"
|
||||||
|
bg = "#220000"
|
||||||
|
|
||||||
|
[boards.house.palette.C]
|
||||||
|
type = "object"
|
||||||
|
script = "chest"
|
||||||
|
name = "chest"
|
||||||
|
enter = "block"
|
||||||
|
tile = 240
|
||||||
|
fg = "#ccaa44"
|
||||||
|
bg = "#222200"
|
||||||
|
|
||||||
|
# Seven of these, so it stays unnamed — object names must be board-unique.
|
||||||
|
[boards.house.palette.T]
|
||||||
|
type = "object"
|
||||||
|
script = "yammerer"
|
||||||
|
enter = "block"
|
||||||
|
tile = 1
|
||||||
|
fg = "#99ff44"
|
||||||
|
bg = "#000000"
|
||||||
|
|
||||||
|
[[boards.house.portals]]
|
||||||
|
x = 56
|
||||||
|
y = 5
|
||||||
|
name = "from_start"
|
||||||
|
target_board = "start"
|
||||||
|
target_name = "to_house"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user