wip 1
This commit is contained in:
+5
-13
@@ -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
@@ -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)]
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user