diff --git a/kiln-core/src/action.rs b/kiln-core/src/action.rs index 94f943d..8042d0e 100644 --- a/kiln-core/src/action.rs +++ b/kiln-core/src/action.rs @@ -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")) } diff --git a/kiln-core/src/board.rs b/kiln-core/src/board.rs index 6d9344f..6f65c29 100644 --- a/kiln-core/src/board.rs +++ b/kiln-core/src/board.rs @@ -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)] diff --git a/kiln-core/src/builtin.rs b/kiln-core/src/builtin.rs index edbabe1..7640818 100644 --- a/kiln-core/src/builtin.rs +++ b/kiln-core/src/builtin.rs @@ -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 }; } diff --git a/kiln-core/src/game.rs b/kiln-core/src/game.rs index 288c4da..6c0395f 100644 --- a/kiln-core/src/game.rs +++ b/kiln-core/src/game.rs @@ -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(); diff --git a/kiln-core/src/script.rs b/kiln-core/src/script.rs index f03b729..405e196 100644 --- a/kiln-core/src/script.rs +++ b/kiln-core/src/script.rs @@ -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()); diff --git a/kiln-core/src/scripts/crate.rhai b/kiln-core/src/scripts/crate.rhai new file mode 100644 index 0000000..17e527e --- /dev/null +++ b/kiln-core/src/scripts/crate.rhai @@ -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); +} diff --git a/kiln-core/src/scripts/gem.rhai b/kiln-core/src/scripts/gem.rhai index 2ba1770..f342950 100644 --- a/kiln-core/src/scripts/gem.rhai +++ b/kiln-core/src/scripts/gem.rhai @@ -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(); } diff --git a/kiln-core/src/scripts/heart.rhai b/kiln-core/src/scripts/heart.rhai index 7d951e1..98074ea 100644 --- a/kiln-core/src/scripts/heart.rhai +++ b/kiln-core/src/scripts/heart.rhai @@ -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(); } diff --git a/kiln-core/src/scripts/key.rhai b/kiln-core/src/scripts/key.rhai index e0e2862..0f05650 100644 --- a/kiln-core/src/scripts/key.rhai +++ b/kiln-core/src/scripts/key.rhai @@ -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", diff --git a/kiln-core/src/utils.rs b/kiln-core/src/utils.rs index 6c06d31..a9fff18 100644 --- a/kiln-core/src/utils.rs +++ b/kiln-core/src/utils.rs @@ -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") + .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") + .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` diff --git a/maps/start.toml b/maps/start.toml index 928f5b5..bac2668 100644 --- a/maps/start.toml +++ b/maps/start.toml @@ -8,7 +8,7 @@ start = "start" greeter = """ fn init(me) { 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!"); 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 -// tick() runs. Queue.length() avoids piling up moves; blocked() avoids walking -// into a wall (or another object's already-queued move this tick). +// tick() runs. The engine only calls tick() once this object's queue has drained, +// so a whole dance can be queued up front without piling up. fn tick(me, dt) { move(East); move(South); move(North); move(East); say("Hey!", 0.5); - delay(me, 0.5); + me.delay(0.5); move(West); move(South); move(North); @@ -159,7 +159,7 @@ fn bump(me, _dir) { yammerer = """ fn tick(me, dt) { 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 -# onto its cell and logs. Triggers are placed via [[boards.NAME.triggers]]. +# A sensor script: no glyph, off the grid — it just watches for the player stepping +# onto its cell and logs. Sensors are placed via [[boards.NAME.sensors]]. tripwire = """ fn enter(me, dir) { 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" width = 60 height = 25 @@ -193,23 +193,24 @@ height = 25 # grass/dirt/stone/water floor is not representable with one floor attribute). floor = { biome = "grass" } -# The single grid: all solids and most non-solids (terrain, the player, objects and -# the portal). A space is always a transparent empty cell, so the floor shows through. -[boards.start.grid] -content = """ +# The grid holds every solid: terrain, crates, the player and scripted objects. A +# space is a transparent empty cell, so the floor shows through. Portals and sensors +# are *not* here — they sit alongside the grid (see the arrays further down), because +# the player has to be able to share their cell. +grid = """ ############################################################ # # # o # # oS # # # -# 1 # +# # # )######( B # # # # -# +++ # v # +# # v # # # # # | # # # -# V oo G - @ # +# V oo - @ # # /# # # t # # # @@ -223,52 +224,99 @@ content = """ # # ############################################################ """ -[boards.start.grid.palette] -"#" = { kind = "wall", tile = "#", fg = "#808080", bg = "#606060" } -"+" = { kind = "banana", tile = "#", fg = "#808080", bg = "#606060" } # unknown kind -> ErrorBlock demo -"o" = { kind = "crate", tile = 254, fg = "#aaaaaa", bg = "#000000" } -"-" = { kind = "hcrate", tile = 29, fg = "#aaaaaa", bg = "#000000" } -"|" = { kind = "vcrate", tile = 18, fg = "#aaaaaa", bg = "#000000" } -">" = { kind = "pusher_east" } -"^" = { kind = "pusher_north" } -"@" = { kind = "player" } -"1" = { kind = "portal", name = "to_house", target_map = "house", target_entry = "from_start" } -"G" = { kind = "object", tile = "#", fg = "#aa3333", bg = "#000000", solid = false, script_name = "greeter" } -"V" = { kind = "object", tile = 1, fg = "#33aa33", bg = "#000000", script_name = "mover" } -"M" = { kind = "object", tile = 15, fg = "#ffdd88", bg = "#000000", solid = true, name = "muffin", script_name = "muffin" } -"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" } +[boards.start.palette] +"#" = { type = "builtin", kind = "wall", tile = "#", fg = "#808080", bg = "#606060" } +"o" = { type = "builtin", kind = "crate", tile = 254, fg = "#aaaaaa", bg = "#000000" } +"-" = { type = "builtin", kind = "hcrate", tile = 29, fg = "#aaaaaa", bg = "#000000" } +"|" = { type = "builtin", kind = "vcrate", tile = 18, fg = "#aaaaaa", bg = "#000000" } +">" = { type = "builtin", kind = "pusher_east" } +"^" = { type = "builtin", kind = "pusher_north" } +"/" = { type = "builtin", kind = "spinner_cw" } +"t" = { type = "builtin", kind = "gem" } +"v" = { type = "builtin", kind = "transporter_south" } +")" = { type = "builtin", kind = "transporter_east" } +"(" = { type = "builtin", kind = "transporter_west" } +"@" = { type = "player" } -# Triggers: invisible, non-solid, script-only objects placed off the grid. This one -# watches for the player and logs when they reach its cell. -[[boards.start.triggers]] +# The dancer: walks a fixed loop, and teleports back to where it started if the +# board Registry remembers a previous position. +[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 y = 12 -script_name = "tripwire" +script = "tripwire" -[boards.house.map] +[boards.house] name = "The House" width = 60 height = 25 dark = true -# A single fixed floor glyph across the whole board. -floor = { tile = 176, fg = "#888888", bg = "#444444" } +# A single fixed floor glyph across the whole board. There are no light sources +# 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. -[boards.house.grid] -content = """ +# The grid: terrain, the player and objects. The return portal is off-grid, below. +grid = """ ############################################################ # # # K ###F### # # # # # # ####### # -# 1 # +# # # # # ######### T # # # # T # @@ -289,12 +337,50 @@ content = """ # # ############################################################ """ -[boards.house.grid.palette] -"#" = { kind = "wall", tile = 178, fg = "#888888", bg = "#555555" } -"@" = { kind = "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" } -"F" = { kind = "object", tile = 15, fg = "#ff8800", bg = "#220000", solid = true, name = "fireplace", script_name = "fireplace" } -"C" = { kind = "object", tile = 240, fg = "#ccaa44", bg = "#222200", solid = true, name = "chest", script_name = "chest" } -"T" = { kind = "object", tile = 1, fg = "#99ff44", bg = "#000000", solid = true, script_name = "yammerer" } +[boards.house.palette] +"#" = { type = "builtin", kind = "wall", tile = 178, fg = "#888888", bg = "#555555" } +"@" = { type = "player" } + +[boards.house.palette.K] +type = "object" +script = "bookshelf" +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"