wip
This commit is contained in:
Generated
+3
@@ -199,6 +199,9 @@ name = "color"
|
|||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf"
|
checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compact_str"
|
name = "compact_str"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
color = "0.3.3"
|
color = { version = "0.3.3", features = ["serde"] }
|
||||||
doryen-fov = "0.1"
|
doryen-fov = "0.1"
|
||||||
rhai = "1"
|
rhai = "1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ use crate::Direction;
|
|||||||
use crate::action::BoardAction;
|
use crate::action::BoardAction;
|
||||||
use crate::object_def::ObjectDef;
|
use crate::object_def::ObjectDef;
|
||||||
use crate::script::Registerable;
|
use crate::script::Registerable;
|
||||||
use crate::utils::{LogSink, ObjectId};
|
use crate::utils::{Behavior, LogSink, ObjectId};
|
||||||
|
|
||||||
/// A snapshot of one board object, returned by `Board.tagged`, `Board.named`,
|
/// A snapshot of one board object, returned by `Board.tagged`, `Board.named`,
|
||||||
/// and `Board.get`. Passed by value — scripts read fields, not a live reference.
|
/// and `Board.get`. Passed by value — scripts read fields, not a live reference.
|
||||||
@@ -116,7 +116,7 @@ impl Registerable for ObjectInfo {
|
|||||||
// me.light: the object's current emitted light radius in cells (0 = none).
|
// me.light: the object's current emitted light radius in cells (0 = none).
|
||||||
engine.register_get("light", |o: &mut ObjectInfo| -> i64 {
|
engine.register_get("light", |o: &mut ObjectInfo| -> i64 {
|
||||||
match o.board.borrow().objects.get(&o.id) {
|
match o.board.borrow().objects.get(&o.id) {
|
||||||
Some(ObjectDef { light, .. }) => *light as i64,
|
Some(ObjectDef { behavior: Behavior { glow, .. }, .. }) => *glow as i64,
|
||||||
None => 0,
|
None => 0,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::script::Registerable;
|
|||||||
use crate::utils::{LogSink, ObjectId};
|
use crate::utils::{LogSink, ObjectId};
|
||||||
|
|
||||||
/// A single object's output queue.
|
/// A single object's output queue.
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Default)]
|
||||||
pub struct ObjQueue(Rc<RefCell<VecDeque<Action>>>);
|
pub struct ObjQueue(Rc<RefCell<VecDeque<Action>>>);
|
||||||
|
|
||||||
impl ObjQueue {
|
impl ObjQueue {
|
||||||
|
|||||||
+50
-84
@@ -2,6 +2,8 @@ use crate::glyph::Glyph;
|
|||||||
use crate::utils::{Behavior, Pushable};
|
use crate::utils::{Behavior, Pushable};
|
||||||
use color::Rgba8;
|
use color::Rgba8;
|
||||||
use crate::keys::KeyType;
|
use crate::keys::KeyType;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use crate::tile::EnterResponse;
|
||||||
|
|
||||||
/// Declares the set of script-backed archetype families.
|
/// Declares the set of script-backed archetype families.
|
||||||
///
|
///
|
||||||
@@ -40,7 +42,7 @@ macro_rules! builtins {
|
|||||||
///
|
///
|
||||||
/// Add one entry to the `builtins!` invocation in `archetype.rs` and write
|
/// Add one entry to the `builtins!` invocation in `archetype.rs` and write
|
||||||
/// `kiln-core/src/scripts/<name>.rhai`. No other files need to change.
|
/// `kiln-core/src/scripts/<name>.rhai`. No other files need to change.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub enum Builtin {
|
pub enum Builtin {
|
||||||
$( $variant ),+
|
$( $variant ),+
|
||||||
}
|
}
|
||||||
@@ -49,7 +51,7 @@ macro_rules! builtins {
|
|||||||
/// Returns `(family_variant, matched_alias)` for `name`, or `None` if `name`
|
/// Returns `(family_variant, matched_alias)` for `name`, or `None` if `name`
|
||||||
/// is not a known builtin keyword. The returned `&'static str` is the exact
|
/// is not a known builtin keyword. The returned `&'static str` is the exact
|
||||||
/// literal from the macro (always valid for a `Archetype::Builtin` field).
|
/// literal from the macro (always valid for a `Archetype::Builtin` field).
|
||||||
pub(crate) fn from_name(name: &str) -> Option<(Self, &'static str)> {
|
pub fn from_name(name: &str) -> Option<(Self, &'static str)> {
|
||||||
match name {
|
match name {
|
||||||
$(
|
$(
|
||||||
$( $name => Some((Builtin::$variant, $name)), )+
|
$( $name => Some((Builtin::$variant, $name)), )+
|
||||||
@@ -59,7 +61,7 @@ macro_rules! builtins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the uniform behavior shared by all aliases in this family.
|
/// Returns the uniform behavior shared by all aliases in this family.
|
||||||
pub(crate) fn behavior(self) -> Behavior {
|
pub fn behavior(self) -> Behavior {
|
||||||
match self {
|
match self {
|
||||||
$( Builtin::$variant => $behavior, )+
|
$( Builtin::$variant => $behavior, )+
|
||||||
}
|
}
|
||||||
@@ -69,7 +71,7 @@ macro_rules! builtins {
|
|||||||
/// so aliases within a family can differ in color (e.g. colored keys).
|
/// so aliases within a family can differ in color (e.g. colored keys).
|
||||||
/// Falls back to a transparent glyph for unrecognized aliases (shouldn't
|
/// Falls back to a transparent glyph for unrecognized aliases (shouldn't
|
||||||
/// happen in practice since aliases are all from the macro).
|
/// happen in practice since aliases are all from the macro).
|
||||||
pub(crate) fn default_glyph_for(self, alias: &str) -> Glyph {
|
pub fn default_glyph_for(self, alias: &str) -> Glyph {
|
||||||
match alias {
|
match alias {
|
||||||
$(
|
$(
|
||||||
$( $name => $glyph, )+
|
$( $name => $glyph, )+
|
||||||
@@ -79,7 +81,7 @@ macro_rules! builtins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the embedded Rhai source shared by all aliases in this family.
|
/// Returns the embedded Rhai source shared by all aliases in this family.
|
||||||
pub(crate) fn script(self) -> &'static str {
|
pub fn script(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
$( Builtin::$variant => $script, )+
|
$( Builtin::$variant => $script, )+
|
||||||
}
|
}
|
||||||
@@ -100,11 +102,11 @@ const fn g(tile: u32, r: u8, gr: u8, b: u8) -> Glyph {
|
|||||||
|
|
||||||
builtins! {
|
builtins! {
|
||||||
Gem => ["gem" => g(4, 0x50, 0x50, 0xFF)] {
|
Gem => ["gem" => g(4, 0x50, 0x50, 0xFF)] {
|
||||||
behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true },
|
behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true, glow: 0 },
|
||||||
script: include_str!("scripts/gem.rhai"),
|
script: include_str!("scripts/gem.rhai"),
|
||||||
},
|
},
|
||||||
Heart => ["heart" => g(3, 0xCC, 0x22, 0x22)] {
|
Heart => ["heart" => g(3, 0xCC, 0x22, 0x22)] {
|
||||||
behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true },
|
behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true, glow: 0 },
|
||||||
script: include_str!("scripts/heart.rhai"),
|
script: include_str!("scripts/heart.rhai"),
|
||||||
},
|
},
|
||||||
Pusher => [
|
Pusher => [
|
||||||
@@ -113,14 +115,14 @@ builtins! {
|
|||||||
"pusher_east" => g(16, 0xAA, 0xAA, 0xAA),
|
"pusher_east" => g(16, 0xAA, 0xAA, 0xAA),
|
||||||
"pusher_west" => g(17, 0xAA, 0xAA, 0xAA),
|
"pusher_west" => g(17, 0xAA, 0xAA, 0xAA),
|
||||||
] {
|
] {
|
||||||
behavior: Behavior { solid: true, opaque: true, pushable: Pushable::No, grab: false },
|
behavior: Behavior { solid: true, opaque: true, pushable: Pushable::No, grab: false, glow: 0 },
|
||||||
script: include_str!("scripts/pusher.rhai"),
|
script: include_str!("scripts/pusher.rhai"),
|
||||||
},
|
},
|
||||||
Spinner => [
|
Spinner => [
|
||||||
"spinner_cw" => g(47, 0xAA, 0xAA, 0xAA),
|
"spinner_cw" => g(47, 0xAA, 0xAA, 0xAA),
|
||||||
"spinner_ccw" => g(92, 0xAA, 0xAA, 0xAA),
|
"spinner_ccw" => g(92, 0xAA, 0xAA, 0xAA),
|
||||||
] {
|
] {
|
||||||
behavior: Behavior { solid: true, opaque: true, pushable: Pushable::No, grab: false },
|
behavior: Behavior { solid: true, opaque: true, pushable: Pushable::No, grab: false, glow: 0 },
|
||||||
script: include_str!("scripts/spinner.rhai"),
|
script: include_str!("scripts/spinner.rhai"),
|
||||||
},
|
},
|
||||||
// Solid, see-through (opaque: false), unpushable teleporters. Each direction's
|
// Solid, see-through (opaque: false), unpushable teleporters. Each direction's
|
||||||
@@ -131,7 +133,7 @@ builtins! {
|
|||||||
"transporter_east" => g(41, 0x55, 0xFF, 0xFF), // ')'
|
"transporter_east" => g(41, 0x55, 0xFF, 0xFF), // ')'
|
||||||
"transporter_west" => g(40, 0x55, 0xFF, 0xFF), // '('
|
"transporter_west" => g(40, 0x55, 0xFF, 0xFF), // '('
|
||||||
] {
|
] {
|
||||||
behavior: Behavior { solid: true, opaque: false, pushable: Pushable::No, grab: false },
|
behavior: Behavior { solid: true, opaque: false, pushable: Pushable::No, grab: false, glow: 0 },
|
||||||
script: include_str!("scripts/transporter.rhai"),
|
script: include_str!("scripts/transporter.rhai"),
|
||||||
},
|
},
|
||||||
Key => [ // TODO these should refer to the key colors in Keyring
|
Key => [ // TODO these should refer to the key colors in Keyring
|
||||||
@@ -144,9 +146,28 @@ builtins! {
|
|||||||
"key_yellow" => KeyType::Yellow.glyph(),
|
"key_yellow" => KeyType::Yellow.glyph(),
|
||||||
"key_white" => KeyType::White.glyph(),
|
"key_white" => KeyType::White.glyph(),
|
||||||
] {
|
] {
|
||||||
behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true },
|
behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true, glow: 0 },
|
||||||
script: include_str!("scripts/key.rhai"),
|
script: include_str!("scripts/key.rhai"),
|
||||||
}
|
},
|
||||||
|
Wall => ["wall" => Glyph {
|
||||||
|
tile: 35,
|
||||||
|
fg: Rgba8 { r: 0x80, g: 0x80, b: 0x80, a: 255 },
|
||||||
|
bg: Rgba8 { r: 0x60, g: 0x60, b: 0x60, a: 255 }}] {
|
||||||
|
behavior: Behavior { solid: true, opaque: true, pushable: Pushable::No, grab: false, glow: 0 },
|
||||||
|
script: ""
|
||||||
|
},
|
||||||
|
Crate => ["crate" => g(254, 0xaa, 0xaa, 0xaa)] { // CP437 ■ (small filled square)
|
||||||
|
behavior: Behavior { solid: true, opaque: true, pushable: Pushable::Any, grab: false, glow: 0 },
|
||||||
|
script: ""
|
||||||
|
},
|
||||||
|
HCrate => ["hcrate" => g(29, 0xaa, 0xaa, 0xaa)] { // CP437 ↔ (left-right arrow) — pushable east/west
|
||||||
|
behavior: Behavior { solid: true, opaque: true, pushable: Pushable::Horizontal, grab: false, glow: 0 },
|
||||||
|
script: ""
|
||||||
|
},
|
||||||
|
VCrate => ["vcrate" => g(18, 0xaa, 0xaa, 0xaa)] { // CP437 ↕ (up-down arrow) — pushable north/south
|
||||||
|
behavior: Behavior { solid: true, opaque: true, pushable: Pushable::Vertical, grab: false, glow: 0 },
|
||||||
|
script: ""
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A class of board cell, encoding its default behavior and appearance.
|
/// A class of board cell, encoding its default behavior and appearance.
|
||||||
@@ -171,22 +192,17 @@ builtins! {
|
|||||||
///
|
///
|
||||||
/// [`ObjectDef`]: crate::object_def::ObjectDef
|
/// [`ObjectDef`]: crate::object_def::ObjectDef
|
||||||
/// [`Board::expand_builtin_archetypes`]: crate::board::Board::expand_builtin_archetypes
|
/// [`Board::expand_builtin_archetypes`]: crate::board::Board::expand_builtin_archetypes
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum Archetype {
|
pub enum Archetype {
|
||||||
/// An open cell; the player and other entities can pass through it.
|
/// An open cell; the player and other entities can pass through it.
|
||||||
Empty,
|
Empty,
|
||||||
/// A solid wall; impassable and opaque.
|
|
||||||
Wall,
|
|
||||||
/// A solid, pushable box. Walking into it shoves it one cell in the same
|
|
||||||
/// direction (cascading through a chain of crates) if there is open space.
|
|
||||||
Crate,
|
|
||||||
/// A crate that can only be pushed east/west (horizontal axis). Glyph ↔.
|
|
||||||
HCrate,
|
|
||||||
/// A crate that can only be pushed north/south (vertical axis). Glyph ↕.
|
|
||||||
VCrate,
|
|
||||||
/// A wall-mounted torch: non-solid, non-opaque decorative terrain that emits
|
/// A wall-mounted torch: non-solid, non-opaque decorative terrain that emits
|
||||||
/// warm light on a `dark` board (radius from [`Archetype::light`]). Glyph ☼.
|
/// warm light on a `dark` board (radius from [`Archetype::light`]). Glyph ☼.
|
||||||
Torch,
|
Torch,
|
||||||
|
/// Sentinel for map files that reference an unknown archetype name.
|
||||||
|
/// Renders as a yellow `?` on red to make the error visible in-game.
|
||||||
|
ErrorBlock,
|
||||||
/// A script-backed archetype expanded from a map-file keyword.
|
/// A script-backed archetype expanded from a map-file keyword.
|
||||||
///
|
///
|
||||||
/// - `Builtin` is the family (e.g. `Builtin::Pusher`), which selects the
|
/// - `Builtin` is the family (e.g. `Builtin::Pusher`), which selects the
|
||||||
@@ -196,10 +212,8 @@ pub enum Archetype {
|
|||||||
/// `BUILTIN_<alias>` tag on the expanded object.
|
/// `BUILTIN_<alias>` tag on the expanded object.
|
||||||
///
|
///
|
||||||
/// See [`Builtin`] and the [`builtins!`] invocation for the full registry.
|
/// See [`Builtin`] and the [`builtins!`] invocation for the full registry.
|
||||||
|
#[serde(untagged)]
|
||||||
Builtin(Builtin, &'static str),
|
Builtin(Builtin, &'static str),
|
||||||
/// Sentinel for map files that reference an unknown archetype name.
|
|
||||||
/// Renders as a yellow `?` on red to make the error visible in-game.
|
|
||||||
ErrorBlock,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Archetype {
|
impl Archetype {
|
||||||
@@ -212,30 +226,7 @@ impl Archetype {
|
|||||||
opaque: false,
|
opaque: false,
|
||||||
pushable: Pushable::No,
|
pushable: Pushable::No,
|
||||||
grab: false,
|
grab: false,
|
||||||
},
|
glow: 0,
|
||||||
Archetype::Wall => Behavior {
|
|
||||||
solid: true,
|
|
||||||
opaque: true,
|
|
||||||
pushable: Pushable::No,
|
|
||||||
grab: false,
|
|
||||||
},
|
|
||||||
Archetype::Crate => Behavior {
|
|
||||||
solid: true,
|
|
||||||
opaque: true,
|
|
||||||
pushable: Pushable::Any,
|
|
||||||
grab: false,
|
|
||||||
},
|
|
||||||
Archetype::HCrate => Behavior {
|
|
||||||
solid: true,
|
|
||||||
opaque: true,
|
|
||||||
pushable: Pushable::Horizontal,
|
|
||||||
grab: false,
|
|
||||||
},
|
|
||||||
Archetype::VCrate => Behavior {
|
|
||||||
solid: true,
|
|
||||||
opaque: true,
|
|
||||||
pushable: Pushable::Vertical,
|
|
||||||
grab: false,
|
|
||||||
},
|
},
|
||||||
// A torch you can walk past and see through; it only lights the room.
|
// A torch you can walk past and see through; it only lights the room.
|
||||||
Archetype::Torch => Behavior {
|
Archetype::Torch => Behavior {
|
||||||
@@ -243,12 +234,14 @@ impl Archetype {
|
|||||||
opaque: false,
|
opaque: false,
|
||||||
pushable: Pushable::No,
|
pushable: Pushable::No,
|
||||||
grab: false,
|
grab: false,
|
||||||
|
glow: 6,
|
||||||
},
|
},
|
||||||
Archetype::ErrorBlock => Behavior {
|
Archetype::ErrorBlock => Behavior {
|
||||||
solid: true,
|
solid: true,
|
||||||
opaque: true,
|
opaque: true,
|
||||||
pushable: Pushable::No,
|
pushable: Pushable::No,
|
||||||
grab: false,
|
grab: false,
|
||||||
|
glow: 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,10 +251,6 @@ impl Archetype {
|
|||||||
match self {
|
match self {
|
||||||
Archetype::Builtin(_, alias) => alias,
|
Archetype::Builtin(_, alias) => alias,
|
||||||
Archetype::Empty => "empty",
|
Archetype::Empty => "empty",
|
||||||
Archetype::Wall => "wall",
|
|
||||||
Archetype::Crate => "crate",
|
|
||||||
Archetype::HCrate => "hcrate",
|
|
||||||
Archetype::VCrate => "vcrate",
|
|
||||||
Archetype::Torch => "torch",
|
Archetype::Torch => "torch",
|
||||||
Archetype::ErrorBlock => "error_block",
|
Archetype::ErrorBlock => "error_block",
|
||||||
}
|
}
|
||||||
@@ -292,26 +281,6 @@ impl Archetype {
|
|||||||
fg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
fg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
||||||
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
||||||
},
|
},
|
||||||
Archetype::Wall => Glyph {
|
|
||||||
tile: 35,
|
|
||||||
fg: Rgba8 { r: 0x80, g: 0x80, b: 0x80, a: 255 },
|
|
||||||
bg: Rgba8 { r: 0x60, g: 0x60, b: 0x60, a: 255 },
|
|
||||||
},
|
|
||||||
Archetype::Crate => Glyph {
|
|
||||||
tile: 254, // CP437 ■ (small filled square)
|
|
||||||
fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 },
|
|
||||||
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
|
||||||
},
|
|
||||||
Archetype::HCrate => Glyph {
|
|
||||||
tile: 29, // CP437 ↔ (left-right arrow) — pushable east/west
|
|
||||||
fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 },
|
|
||||||
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
|
||||||
},
|
|
||||||
Archetype::VCrate => Glyph {
|
|
||||||
tile: 18, // CP437 ↕ (up-down arrow) — pushable north/south
|
|
||||||
fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 },
|
|
||||||
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
|
||||||
},
|
|
||||||
Archetype::Torch => Glyph {
|
Archetype::Torch => Glyph {
|
||||||
tile: 15, // CP437 ☼ (sun) — a warm point of light
|
tile: 15, // CP437 ☼ (sun) — a warm point of light
|
||||||
fg: Rgba8 { r: 0xFF, g: 0xB0, b: 0x40, a: 255 }, // warm amber (also its light color)
|
fg: Rgba8 { r: 0xFF, g: 0xB0, b: 0x40, a: 255 }, // warm amber (also its light color)
|
||||||
@@ -343,14 +312,17 @@ impl TryFrom<&str> for Archetype {
|
|||||||
}
|
}
|
||||||
match name {
|
match name {
|
||||||
"empty" => Ok(Archetype::Empty),
|
"empty" => Ok(Archetype::Empty),
|
||||||
"wall" => Ok(Archetype::Wall),
|
|
||||||
"crate" => Ok(Archetype::Crate),
|
|
||||||
"hcrate" => Ok(Archetype::HCrate),
|
|
||||||
"vcrate" => Ok(Archetype::VCrate),
|
|
||||||
"torch" => Ok(Archetype::Torch),
|
"torch" => Ok(Archetype::Torch),
|
||||||
// "object", "portal", "player" are intentionally absent: they are
|
// "object", "portal", "player" are intentionally absent: they are
|
||||||
// meta-kinds handled by the layer builder, not Archetype variants.
|
// meta-kinds handled by the layer builder, not Archetype variants.
|
||||||
_ => Err(format!("unknown archetype: {name}")),
|
_ => {
|
||||||
|
// is it a valid builtin?
|
||||||
|
if let Some((b, s)) = Builtin::from_name(name) {
|
||||||
|
Ok(Archetype::Builtin(b, s))
|
||||||
|
} else {
|
||||||
|
Err(format!("unknown archetype: {name}"))
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,12 +331,6 @@ impl TryFrom<&str> for Archetype {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::Archetype;
|
use super::Archetype;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn directional_crate_default_glyph_tiles() {
|
|
||||||
assert_eq!(Archetype::HCrate.default_glyph().tile, 29);
|
|
||||||
assert_eq!(Archetype::VCrate.default_glyph().tile, 18);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builtin_names_glyphs_and_round_trip() {
|
fn builtin_names_glyphs_and_round_trip() {
|
||||||
// All known aliases must parse, round-trip via name(), and give the right tile.
|
// All known aliases must parse, round-trip via name(), and give the right tile.
|
||||||
|
|||||||
+68
-71
@@ -5,8 +5,9 @@ use crate::glyph::Glyph;
|
|||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
use crate::object_def::ObjectDef;
|
use crate::object_def::ObjectDef;
|
||||||
use crate::utils::Direction;
|
use crate::utils::Direction;
|
||||||
use crate::utils::{Behavior, ObjectId, PlayerPos, PortalDef, Pushable, RegistryValue, Solid};
|
use crate::utils::{ObjectId, PlayerPos, PortalDef, Pushable, RegistryValue, Solid};
|
||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
use crate::tile::{EnterResponse, Sensor};
|
||||||
|
|
||||||
/// The result of [`Board::apply_shift`]: any error lines to log, plus the
|
/// The result of [`Board::apply_shift`]: any error lines to log, plus the
|
||||||
/// `(from, to)` cell relocations the shift actually performed.
|
/// `(from, to)` cell relocations the shift actually performed.
|
||||||
@@ -84,6 +85,8 @@ pub struct Board {
|
|||||||
/// Non-solid things placed off the main grid, drawn only where the grid cell is
|
/// Non-solid things placed off the main grid, drawn only where the grid cell is
|
||||||
/// empty (see [`Decoration`]). Normally empty; populated by save files.
|
/// empty (see [`Decoration`]). Normally empty; populated by save files.
|
||||||
pub(crate) decorations: Vec<Decoration>,
|
pub(crate) decorations: Vec<Decoration>,
|
||||||
|
/// Non-solid things placed off the main grid, can't affect movement but see other hooks
|
||||||
|
pub sensors: Vec<Sensor>,
|
||||||
/// Current player position on this board. See [`PlayerPos`] for caveats
|
/// Current player position on this board. See [`PlayerPos`] for caveats
|
||||||
/// about its future. Game-global player *stats* live in [`crate::player::Player`].
|
/// about its future. Game-global player *stats* live in [`crate::player::Player`].
|
||||||
pub player: PlayerPos,
|
pub player: PlayerPos,
|
||||||
@@ -176,7 +179,7 @@ impl Board {
|
|||||||
// non-solid object (lets invisible trigger objects exist).
|
// non-solid object (lets invisible trigger objects exist).
|
||||||
let mut nonsolid: Option<Glyph> = None;
|
let mut nonsolid: Option<Glyph> = None;
|
||||||
for o in self.objects.values().filter(|o| o.x == x && o.y == y) {
|
for o in self.objects.values().filter(|o| o.x == x && o.y == y) {
|
||||||
if o.solid {
|
if o.behavior.solid {
|
||||||
return o.glyph;
|
return o.glyph;
|
||||||
}
|
}
|
||||||
if nonsolid.is_none() && o.glyph.tile != 0 {
|
if nonsolid.is_none() && o.glyph.tile != 0 {
|
||||||
@@ -245,17 +248,7 @@ impl Board {
|
|||||||
// A solid object shadows the cell it sits on; capture its behavior now.
|
// A solid object shadows the cell it sits on; capture its behavior now.
|
||||||
if let Some(id) = self.solid_object_id_at(x, y) {
|
if let Some(id) = self.solid_object_id_at(x, y) {
|
||||||
let obj = &self.objects[&id];
|
let obj = &self.objects[&id];
|
||||||
let behavior = Behavior {
|
let behavior = obj.behavior;
|
||||||
solid: obj.solid,
|
|
||||||
opaque: obj.opaque,
|
|
||||||
// ObjectDef stores pushability as a bool meaning "any direction".
|
|
||||||
pushable: if obj.pushable {
|
|
||||||
Pushable::Any
|
|
||||||
} else {
|
|
||||||
Pushable::No
|
|
||||||
},
|
|
||||||
grab: obj.grab,
|
|
||||||
};
|
|
||||||
return Some(Solid::object_at(x, y, id, behavior));
|
return Some(Solid::object_at(x, y, id, behavior));
|
||||||
}
|
}
|
||||||
// Otherwise the grid cell's terrain archetype may be solid (e.g. a wall).
|
// Otherwise the grid cell's terrain archetype may be solid (e.g. a wall).
|
||||||
@@ -282,7 +275,7 @@ impl Board {
|
|||||||
/// Panics if `x` or `y` are out of bounds.
|
/// Panics if `x` or `y` are out of bounds.
|
||||||
pub fn is_opaque_at(&self, x: usize, y: usize) -> bool {
|
pub fn is_opaque_at(&self, x: usize, y: usize) -> bool {
|
||||||
self.get(x, y).1.behavior().opaque
|
self.get(x, y).1.behavior().opaque
|
||||||
|| self.objects.values().any(|o| o.x == x && o.y == y && o.opaque)
|
|| self.objects.values().any(|o| o.x == x && o.y == y && o.behavior.opaque)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Computes lighting + line-of-sight for the player on this board.
|
/// Computes lighting + line-of-sight for the player on this board.
|
||||||
@@ -327,8 +320,8 @@ impl Board {
|
|||||||
}
|
}
|
||||||
// Light-emitting objects: color = their own glyph foreground.
|
// Light-emitting objects: color = their own glyph foreground.
|
||||||
for o in self.objects.values() {
|
for o in self.objects.values() {
|
||||||
if o.light > 0 {
|
if o.behavior.glow > 0 {
|
||||||
add_source(&mut lighting, o.x, o.y, o.light, color_to_rgb(o.glyph.fg));
|
add_source(&mut lighting, o.x, o.y, o.behavior.glow, color_to_rgb(o.glyph.fg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Glowing terrain (e.g. a `Torch` cell): color = the cell's glyph foreground.
|
// Glowing terrain (e.g. a `Torch` cell): color = the cell's glyph foreground.
|
||||||
@@ -377,7 +370,16 @@ impl Board {
|
|||||||
let solid = self.solid_at(cx, cy)?;
|
let solid = self.solid_at(cx, cy)?;
|
||||||
// A solid object — directly, or at the end of a crate chain — is the target.
|
// A solid object — directly, or at the end of a crate chain — is the target.
|
||||||
if let Some(id) = solid.object_id() {
|
if let Some(id) = solid.object_id() {
|
||||||
return Some(id);
|
let obj = self.objects.get(&id)?;
|
||||||
|
let resp = EnterResponse::from(obj.behavior);
|
||||||
|
if let EnterResponse::Push(p) = resp && !p.allows(dir) {
|
||||||
|
// This object can't be pushed that way, so, this is a bump. If it's pushable, the
|
||||||
|
// default push behavior happens (it just gets pushed)
|
||||||
|
return Some(id);
|
||||||
|
} else if resp == EnterResponse::Block {
|
||||||
|
// It's just a wall, block everything
|
||||||
|
return Some(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// The player is never itself "bumped"; a wall (non-pushable terrain) stops
|
// The player is never itself "bumped"; a wall (non-pushable terrain) stops
|
||||||
// the chain with no object behind it. Only a pushable crate is walked
|
// the chain with no object behind it. Only a pushable crate is walked
|
||||||
@@ -528,7 +530,7 @@ impl Board {
|
|||||||
pub fn non_solid_object_ids_at(&self, x: usize, y: usize) -> Vec<ObjectId> {
|
pub fn non_solid_object_ids_at(&self, x: usize, y: usize) -> Vec<ObjectId> {
|
||||||
self.objects
|
self.objects
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, o)| o.x == x && o.y == y && !o.solid)
|
.filter(|(_, o)| o.x == x && o.y == y && !o.behavior.solid)
|
||||||
.map(|(&id, _)| id)
|
.map(|(&id, _)| id)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -536,7 +538,7 @@ impl Board {
|
|||||||
/// Returns a borrow of the actual object at `(x, y)` if any
|
/// Returns a borrow of the actual object at `(x, y)` if any
|
||||||
pub fn solid_object_id_at(&self, x: usize, y: usize) -> Option<ObjectId> {
|
pub fn solid_object_id_at(&self, x: usize, y: usize) -> Option<ObjectId> {
|
||||||
self.objects.iter().find_map(|(&id, o)| {
|
self.objects.iter().find_map(|(&id, o)| {
|
||||||
if o.x == x && o.y == y && o.solid {
|
if o.x == x && o.y == y && o.behavior.solid {
|
||||||
Some(id)
|
Some(id)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -551,7 +553,7 @@ impl Board {
|
|||||||
/// the object's `grab()` hook fires instead.
|
/// the object's `grab()` hook fires instead.
|
||||||
pub fn grab_object_at(&self, x: usize, y: usize) -> Option<ObjectId> {
|
pub fn grab_object_at(&self, x: usize, y: usize) -> Option<ObjectId> {
|
||||||
self.objects.iter().find_map(|(&id, o)| {
|
self.objects.iter().find_map(|(&id, o)| {
|
||||||
if o.x == x && o.y == y && o.solid && o.grab {
|
if o.x == x && o.y == y && EnterResponse::from(o.behavior) == EnterResponse::Grab {
|
||||||
Some(id)
|
Some(id)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -645,13 +647,7 @@ impl Board {
|
|||||||
let beh = b.behavior();
|
let beh = b.behavior();
|
||||||
let mut obj = ObjectDef::new(x, y);
|
let mut obj = ObjectDef::new(x, y);
|
||||||
obj.glyph = glyph;
|
obj.glyph = glyph;
|
||||||
obj.solid = beh.solid;
|
obj.behavior = beh;
|
||||||
obj.opaque = beh.opaque;
|
|
||||||
// Carry the archetype's pushability/grab onto the object (pushers and
|
|
||||||
// spinners are Pushable::No, so they stay unpushable; gems are pushable
|
|
||||||
// and grabbable).
|
|
||||||
obj.pushable = beh.pushable != crate::utils::Pushable::No;
|
|
||||||
obj.grab = beh.grab;
|
|
||||||
obj.builtin_script = Some(b.script());
|
obj.builtin_script = Some(b.script());
|
||||||
// The compile-cache key and the BUILTIN_* tag are both derived from the
|
// The compile-cache key and the BUILTIN_* tag are both derived from the
|
||||||
// alias (e.g. "BUILTIN_pusher_east"), so each alias gets its own cached
|
// alias (e.g. "BUILTIN_pusher_east"), so each alias gets its own cached
|
||||||
@@ -758,7 +754,7 @@ pub(crate) mod tests {
|
|||||||
use crate::floor::Floor;
|
use crate::floor::Floor;
|
||||||
use crate::glyph::Glyph;
|
use crate::glyph::Glyph;
|
||||||
use crate::object_def::ObjectDef;
|
use crate::object_def::ObjectDef;
|
||||||
use crate::utils::Direction;
|
use crate::utils::{Direction, Pushable};
|
||||||
use crate::utils::{ObjectId, PlayerPos};
|
use crate::utils::{ObjectId, PlayerPos};
|
||||||
use color::Rgba8;
|
use color::Rgba8;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
@@ -789,6 +785,7 @@ pub(crate) mod tests {
|
|||||||
grid: vec![(Glyph::transparent(), Archetype::Empty); w * h],
|
grid: vec![(Glyph::transparent(), Archetype::Empty); w * h],
|
||||||
floor: Floor::Blank,
|
floor: Floor::Blank,
|
||||||
decorations: Vec::new(),
|
decorations: Vec::new(),
|
||||||
|
sensors: Vec::new(),
|
||||||
player: PlayerPos {
|
player: PlayerPos {
|
||||||
x: player.0,
|
x: player.0,
|
||||||
y: player.1,
|
y: player.1,
|
||||||
@@ -811,12 +808,12 @@ pub(crate) mod tests {
|
|||||||
|
|
||||||
/// Stamps a crate cell onto the grid.
|
/// Stamps a crate cell onto the grid.
|
||||||
pub(crate) fn crate_at(board: &mut Board, x: usize, y: usize) {
|
pub(crate) fn crate_at(board: &mut Board, x: usize, y: usize) {
|
||||||
*board.get_mut(x, y) = (Archetype::Crate.default_glyph(), Archetype::Crate);
|
*board.get_mut(x, y) = (Archetype::Builtin(Builtin::Crate, "crate").default_glyph(), Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stamps a wall cell onto the grid.
|
/// Stamps a wall cell onto the grid.
|
||||||
pub(crate) fn wall_at(board: &mut Board, x: usize, y: usize) {
|
pub(crate) fn wall_at(board: &mut Board, x: usize, y: usize) {
|
||||||
*board.get_mut(x, y) = (Archetype::Wall.default_glyph(), Archetype::Wall);
|
*board.get_mut(x, y) = (Builtin::Wall.default_glyph_for("wall"), Archetype::Builtin(Builtin::Wall, "wall"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stamps an arbitrary archetype cell onto the grid.
|
/// Stamps an arbitrary archetype cell onto the grid.
|
||||||
@@ -824,7 +821,7 @@ pub(crate) mod tests {
|
|||||||
*board.get_mut(x, y) = (arch.default_glyph(), arch);
|
*board.get_mut(x, y) = (arch.default_glyph(), arch);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn solid_at_reports_wall_object_and_empty() {
|
fn solid_at_reports_wall_object_and_empty() {
|
||||||
let mut board = open_board(4, 1, (3, 0), vec![]);
|
let mut board = open_board(4, 1, (3, 0), vec![]);
|
||||||
wall_at(&mut board, 1, 0);
|
wall_at(&mut board, 1, 0);
|
||||||
@@ -834,7 +831,7 @@ pub(crate) mod tests {
|
|||||||
assert!(board.is_passable(0, 0));
|
assert!(board.is_passable(0, 0));
|
||||||
|
|
||||||
let wall = board.solid_at(1, 0).expect("a wall is solid");
|
let wall = board.solid_at(1, 0).expect("a wall is solid");
|
||||||
assert_eq!(wall.archetype(), Some(Archetype::Wall));
|
assert_eq!(wall.archetype(), Some(Archetype::Builtin(Builtin::Wall, "wall")));
|
||||||
assert!(!board.is_passable(1, 0));
|
assert!(!board.is_passable(1, 0));
|
||||||
|
|
||||||
let obj = board.solid_at(2, 0).expect("an object is solid");
|
let obj = board.solid_at(2, 0).expect("an object is solid");
|
||||||
@@ -847,8 +844,8 @@ pub(crate) mod tests {
|
|||||||
fn grab_object_at_detects_a_gem() {
|
fn grab_object_at_detects_a_gem() {
|
||||||
// A grabbable gem object at (1,0); the player at (2,0).
|
// A grabbable gem object at (1,0); the player at (2,0).
|
||||||
let mut gem = ObjectDef::new(1, 0);
|
let mut gem = ObjectDef::new(1, 0);
|
||||||
gem.grab = true;
|
gem.behavior.grab = true;
|
||||||
gem.pushable = true;
|
gem.behavior.pushable = Pushable::Any;
|
||||||
let board = open_board(3, 1, (2, 0), vec![gem]);
|
let board = open_board(3, 1, (2, 0), vec![gem]);
|
||||||
|
|
||||||
// grab_object_at finds the gem on its own cell, nowhere else.
|
// grab_object_at finds the gem on its own cell, nowhere else.
|
||||||
@@ -859,7 +856,7 @@ pub(crate) mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn non_solid_object_does_not_block() {
|
fn non_solid_object_does_not_block() {
|
||||||
let mut obj = ObjectDef::new(1, 0);
|
let mut obj = ObjectDef::new(1, 0);
|
||||||
obj.solid = false;
|
obj.behavior.solid = false;
|
||||||
let board = open_board(3, 1, (0, 0), vec![obj]);
|
let board = open_board(3, 1, (0, 0), vec![obj]);
|
||||||
assert!(board.solid_at(1, 0).is_none());
|
assert!(board.solid_at(1, 0).is_none());
|
||||||
assert!(board.is_passable(1, 0));
|
assert!(board.is_passable(1, 0));
|
||||||
@@ -891,7 +888,7 @@ pub(crate) mod tests {
|
|||||||
let mut board = open_board(3, 1, (0, 0), vec![]);
|
let mut board = open_board(3, 1, (0, 0), vec![]);
|
||||||
crate_at(&mut board, 1, 0);
|
crate_at(&mut board, 1, 0);
|
||||||
assert!(board.can_push(1, 0, Direction::East));
|
assert!(board.can_push(1, 0, Direction::East));
|
||||||
assert_eq!(board.get(1, 0).1, Archetype::Crate); // no mutation
|
assert_eq!(board.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // no mutation
|
||||||
assert_eq!(board.get(2, 0).1, Archetype::Empty);
|
assert_eq!(board.get(2, 0).1, Archetype::Empty);
|
||||||
|
|
||||||
let mut board = open_board(3, 1, (0, 0), vec![]);
|
let mut board = open_board(3, 1, (0, 0), vec![]);
|
||||||
@@ -909,7 +906,7 @@ pub(crate) mod tests {
|
|||||||
// Crate with open space ahead: shiftable.
|
// Crate with open space ahead: shiftable.
|
||||||
crate_at(&mut board, 0, 0);
|
crate_at(&mut board, 0, 0);
|
||||||
assert!(board.can_shift(0, 0, Direction::East));
|
assert!(board.can_shift(0, 0, Direction::East));
|
||||||
assert_eq!(board.get(0, 0).1, Archetype::Crate); // read-only
|
assert_eq!(board.get(0, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // read-only
|
||||||
|
|
||||||
// Crate with another pushable crate ahead: still shiftable (unlike can_push,
|
// Crate with another pushable crate ahead: still shiftable (unlike can_push,
|
||||||
// which would follow the chain to the wall and fail).
|
// which would follow the chain to the wall and fail).
|
||||||
@@ -937,8 +934,8 @@ pub(crate) mod tests {
|
|||||||
// The player is always a blocker for a shift — even a grab gem may not shift
|
// The player is always a blocker for a shift — even a grab gem may not shift
|
||||||
// onto it (grab now fires only on player movement, not on being shifted in).
|
// onto it (grab now fires only on player movement, not on being shifted in).
|
||||||
let mut gem = ObjectDef::new(0, 0);
|
let mut gem = ObjectDef::new(0, 0);
|
||||||
gem.grab = true;
|
gem.behavior.grab = true;
|
||||||
gem.pushable = true;
|
gem.behavior.pushable = Pushable::Any;
|
||||||
let board = open_board(2, 1, (1, 0), vec![gem]);
|
let board = open_board(2, 1, (1, 0), vec![gem]);
|
||||||
assert!(!board.can_shift(0, 0, Direction::East));
|
assert!(!board.can_shift(0, 0, Direction::East));
|
||||||
|
|
||||||
@@ -956,7 +953,7 @@ pub(crate) mod tests {
|
|||||||
assert!(board.can_push(1, 0, Direction::East));
|
assert!(board.can_push(1, 0, Direction::East));
|
||||||
board.push(1, 0, Direction::East);
|
board.push(1, 0, Direction::East);
|
||||||
assert_eq!(board.get(1, 0).1, Archetype::Empty);
|
assert_eq!(board.get(1, 0).1, Archetype::Empty);
|
||||||
assert_eq!(board.get(2, 0).1, Archetype::Crate);
|
assert_eq!(board.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
assert_eq!((board.player.x, board.player.y), (3, 0));
|
assert_eq!((board.player.x, board.player.y), (3, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -968,7 +965,7 @@ pub(crate) mod tests {
|
|||||||
wall_at(&mut board, 3, 0);
|
wall_at(&mut board, 3, 0);
|
||||||
assert!(!board.can_push(1, 0, Direction::East));
|
assert!(!board.can_push(1, 0, Direction::East));
|
||||||
board.push(1, 0, Direction::East); // no-op
|
board.push(1, 0, Direction::East); // no-op
|
||||||
assert_eq!(board.get(1, 0).1, Archetype::Crate);
|
assert_eq!(board.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
assert_eq!((board.player.x, board.player.y), (2, 0));
|
assert_eq!((board.player.x, board.player.y), (2, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -995,7 +992,7 @@ pub(crate) mod tests {
|
|||||||
add_floor(&mut board, floor_glyph);
|
add_floor(&mut board, floor_glyph);
|
||||||
wall_at(&mut board, 0, 0);
|
wall_at(&mut board, 0, 0);
|
||||||
// The wall (solid) draws over the floor; the empty cell reveals the floor.
|
// The wall (solid) draws over the floor; the empty cell reveals the floor.
|
||||||
assert_eq!(board.glyph_at(0, 0), Archetype::Wall.default_glyph());
|
assert_eq!(board.glyph_at(0, 0), Builtin::Wall.default_glyph_for("wall"));
|
||||||
assert_eq!(board.glyph_at(1, 0), floor_glyph);
|
assert_eq!(board.glyph_at(1, 0), floor_glyph);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1009,11 +1006,11 @@ pub(crate) mod tests {
|
|||||||
};
|
};
|
||||||
add_floor(&mut board, floor);
|
add_floor(&mut board, floor);
|
||||||
|
|
||||||
let wall = Archetype::Wall.default_glyph();
|
let wall = Builtin::Wall.default_glyph_for("wall");
|
||||||
board.place_archetype(1, 0, Archetype::Wall, wall);
|
board.place_archetype(1, 0, Archetype::Builtin(Builtin::Wall, "wall"), wall);
|
||||||
|
|
||||||
// The wall landed on the grid; the floor attribute is untouched.
|
// The wall landed on the grid; the floor attribute is untouched.
|
||||||
assert_eq!(board.get(1, 0), &(wall, Archetype::Wall));
|
assert_eq!(board.get(1, 0), &(wall, Archetype::Builtin(Builtin::Wall, "wall")));
|
||||||
// The solid object that was there is gone.
|
// The solid object that was there is gone.
|
||||||
assert!(board.object_ids_at(1, 0).is_empty());
|
assert!(board.object_ids_at(1, 0).is_empty());
|
||||||
assert_eq!(board.glyph_at(1, 0), wall);
|
assert_eq!(board.glyph_at(1, 0), wall);
|
||||||
@@ -1024,16 +1021,16 @@ pub(crate) mod tests {
|
|||||||
// A crate already occupies the grid at (1,0).
|
// A crate already occupies the grid at (1,0).
|
||||||
let mut board = open_board(3, 1, (2, 0), vec![]);
|
let mut board = open_board(3, 1, (2, 0), vec![]);
|
||||||
crate_at(&mut board, 1, 0);
|
crate_at(&mut board, 1, 0);
|
||||||
let wall = Archetype::Wall.default_glyph();
|
let wall = Builtin::Wall.default_glyph_for("wall");
|
||||||
board.place_archetype(1, 0, Archetype::Wall, wall);
|
board.place_archetype(1, 0, Archetype::Builtin(Builtin::Wall, "wall"), wall);
|
||||||
assert_eq!(board.get(1, 0), &(wall, Archetype::Wall));
|
assert_eq!(board.get(1, 0), &(wall, Archetype::Builtin(Builtin::Wall, "wall")));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn erase_removes_terrain_and_objects_but_keeps_floor() {
|
fn erase_removes_terrain_and_objects_but_keeps_floor() {
|
||||||
// A fixed floor attribute, a wall on the grid, and a (non-solid) object at (1,0).
|
// A fixed floor attribute, a wall on the grid, and a (non-solid) object at (1,0).
|
||||||
let mut obj = ObjectDef::new(1, 0);
|
let mut obj = ObjectDef::new(1, 0);
|
||||||
obj.solid = false;
|
obj.behavior.solid = false;
|
||||||
let mut board = open_board(3, 1, (2, 0), vec![obj]);
|
let mut board = open_board(3, 1, (2, 0), vec![obj]);
|
||||||
let floor = Glyph {
|
let floor = Glyph {
|
||||||
tile: '.' as u32,
|
tile: '.' as u32,
|
||||||
@@ -1072,7 +1069,7 @@ pub(crate) mod tests {
|
|||||||
assert_eq!(board.get(0, 0).1, Archetype::Empty);
|
assert_eq!(board.get(0, 0).1, Archetype::Empty);
|
||||||
let obj = board.objects.values().next().expect("spinner object");
|
let obj = board.objects.values().next().expect("spinner object");
|
||||||
assert_eq!((obj.x, obj.y), (0, 0));
|
assert_eq!((obj.x, obj.y), (0, 0));
|
||||||
assert!(obj.solid);
|
assert!(obj.behavior.solid);
|
||||||
assert!(obj.builtin_script.is_some());
|
assert!(obj.builtin_script.is_some());
|
||||||
assert!(obj.tags.contains("BUILTIN_spinner_cw"));
|
assert!(obj.tags.contains("BUILTIN_spinner_cw"));
|
||||||
|
|
||||||
@@ -1098,7 +1095,7 @@ pub(crate) mod tests {
|
|||||||
crate_at(&mut board, 0, 0);
|
crate_at(&mut board, 0, 0);
|
||||||
let errs = board.apply_shift(&[(0, 0), (9, 0)]);
|
let errs = board.apply_shift(&[(0, 0), (9, 0)]);
|
||||||
assert_eq!(errs.errors.len(), 1);
|
assert_eq!(errs.errors.len(), 1);
|
||||||
assert_eq!(board.get(0, 0).1, Archetype::Crate); // unchanged
|
assert_eq!(board.get(0, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // unchanged
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1122,11 +1119,11 @@ pub(crate) mod tests {
|
|||||||
// Crate(3,0)→(4,0), Crate(4,0)→(0,0) wrap
|
// Crate(3,0)→(4,0), Crate(4,0)→(0,0) wrap
|
||||||
let _errs = board.apply_shift(&[(0, 0), (1, 0), (2, 0), (3, 0), (4, 0)]);
|
let _errs = board.apply_shift(&[(0, 0), (1, 0), (2, 0), (3, 0), (4, 0)]);
|
||||||
|
|
||||||
assert_eq!(board.get(0, 0).1, Archetype::Crate); // wrapped from (4,0)
|
assert_eq!(board.get(0, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // wrapped from (4,0)
|
||||||
assert_eq!(board.get(1, 0).1, Archetype::Crate); // moved from (0,0)
|
assert_eq!(board.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // moved from (0,0)
|
||||||
assert_eq!(board.get(2, 0).1, Archetype::Wall); // blocked, immobile
|
assert_eq!(board.get(2, 0).1, Archetype::Builtin(Builtin::Wall, "wall")); // blocked, immobile
|
||||||
assert_eq!(board.get(3, 0).1, Archetype::Empty); // cleared, crate moved
|
assert_eq!(board.get(3, 0).1, Archetype::Empty); // cleared, crate moved
|
||||||
assert_eq!(board.get(4, 0).1, Archetype::Crate); // moved from (3,0)
|
assert_eq!(board.get(4, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // moved from (3,0)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1139,21 +1136,21 @@ pub(crate) mod tests {
|
|||||||
// Subcase A: HCrate moves
|
// Subcase A: HCrate moves
|
||||||
{
|
{
|
||||||
let mut board = open_board(4, 1, (3, 0), vec![]);
|
let mut board = open_board(4, 1, (3, 0), vec![]);
|
||||||
stamp(&mut board, 0, 0, Archetype::HCrate);
|
stamp(&mut board, 0, 0, Archetype::Builtin(Builtin::HCrate, "hcrate"));
|
||||||
crate_at(&mut board, 1, 0);
|
crate_at(&mut board, 1, 0);
|
||||||
// (2,0) empty
|
// (2,0) empty
|
||||||
|
|
||||||
let _errs = board.apply_shift(&[(0, 0), (1, 0), (2, 0)]);
|
let _errs = board.apply_shift(&[(0, 0), (1, 0), (2, 0)]);
|
||||||
|
|
||||||
assert_eq!(board.get(0, 0).1, Archetype::Empty); // HCrate moved out
|
assert_eq!(board.get(0, 0).1, Archetype::Empty); // HCrate moved out
|
||||||
assert_eq!(board.get(1, 0).1, Archetype::HCrate); // moved from (0,0)
|
assert_eq!(board.get(1, 0).1, Archetype::Builtin(Builtin::HCrate, "hcrate")); // moved from (0,0)
|
||||||
assert_eq!(board.get(2, 0).1, Archetype::Crate); // moved from (1,0)
|
assert_eq!(board.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // moved from (1,0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subcase B: VCrate stays; Crate behind it still moves
|
// Subcase B: VCrate stays; Crate behind it still moves
|
||||||
{
|
{
|
||||||
let mut board = open_board(4, 1, (3, 0), vec![]);
|
let mut board = open_board(4, 1, (3, 0), vec![]);
|
||||||
stamp(&mut board, 0, 0, Archetype::VCrate);
|
stamp(&mut board, 0, 0, Archetype::Builtin(Builtin::VCrate, "vcrate"));
|
||||||
crate_at(&mut board, 1, 0);
|
crate_at(&mut board, 1, 0);
|
||||||
// (2,0) empty
|
// (2,0) empty
|
||||||
|
|
||||||
@@ -1163,9 +1160,9 @@ pub(crate) mod tests {
|
|||||||
// Crate at (1,0) is NOT in blocked, so it moves.
|
// Crate at (1,0) is NOT in blocked, so it moves.
|
||||||
let _errs = board.apply_shift(&[(0, 0), (1, 0), (2, 0)]);
|
let _errs = board.apply_shift(&[(0, 0), (1, 0), (2, 0)]);
|
||||||
|
|
||||||
assert_eq!(board.get(0, 0).1, Archetype::VCrate); // immobile
|
assert_eq!(board.get(0, 0).1, Archetype::Builtin(Builtin::VCrate, "vcrate")); // immobile
|
||||||
assert_eq!(board.get(1, 0).1, Archetype::Empty); // crate moved out
|
assert_eq!(board.get(1, 0).1, Archetype::Empty); // crate moved out
|
||||||
assert_eq!(board.get(2, 0).1, Archetype::Crate); // moved from (1,0)
|
assert_eq!(board.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // moved from (1,0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1178,22 +1175,22 @@ pub(crate) mod tests {
|
|||||||
// Subcase A: VCrate moves
|
// Subcase A: VCrate moves
|
||||||
{
|
{
|
||||||
let mut board = open_board(1, 4, (0, 3), vec![]);
|
let mut board = open_board(1, 4, (0, 3), vec![]);
|
||||||
stamp(&mut board, 0, 0, Archetype::VCrate);
|
stamp(&mut board, 0, 0, Archetype::Builtin(Builtin::VCrate, "vcrate"));
|
||||||
stamp(&mut board, 0, 1, Archetype::Crate);
|
stamp(&mut board, 0, 1, Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
// (0,2) empty
|
// (0,2) empty
|
||||||
|
|
||||||
let _errs = board.apply_shift(&[(0, 0), (0, 1), (0, 2)]);
|
let _errs = board.apply_shift(&[(0, 0), (0, 1), (0, 2)]);
|
||||||
|
|
||||||
assert_eq!(board.get(0, 0).1, Archetype::Empty); // VCrate moved out
|
assert_eq!(board.get(0, 0).1, Archetype::Empty); // VCrate moved out
|
||||||
assert_eq!(board.get(0, 1).1, Archetype::VCrate); // moved from (0,0)
|
assert_eq!(board.get(0, 1).1, Archetype::Builtin(Builtin::VCrate, "vcrate")); // moved from (0,0)
|
||||||
assert_eq!(board.get(0, 2).1, Archetype::Crate); // moved from (0,1)
|
assert_eq!(board.get(0, 2).1, Archetype::Builtin(Builtin::Crate, "crate")); // moved from (0,1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subcase B: HCrate stays; Crate behind it still moves
|
// Subcase B: HCrate stays; Crate behind it still moves
|
||||||
{
|
{
|
||||||
let mut board = open_board(1, 4, (0, 3), vec![]);
|
let mut board = open_board(1, 4, (0, 3), vec![]);
|
||||||
stamp(&mut board, 0, 0, Archetype::HCrate);
|
stamp(&mut board, 0, 0, Archetype::Builtin(Builtin::HCrate, "hcrate"));
|
||||||
stamp(&mut board, 0, 1, Archetype::Crate);
|
stamp(&mut board, 0, 1, Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
// (0,2) empty
|
// (0,2) empty
|
||||||
|
|
||||||
// HCrate (idx 0): target (0,1), origin (0,0), vmove=true.
|
// HCrate (idx 0): target (0,1), origin (0,0), vmove=true.
|
||||||
@@ -1202,9 +1199,9 @@ pub(crate) mod tests {
|
|||||||
// Crate at (0,1) is NOT in blocked, so it moves.
|
// Crate at (0,1) is NOT in blocked, so it moves.
|
||||||
let _errs = board.apply_shift(&[(0, 0), (0, 1), (0, 2)]);
|
let _errs = board.apply_shift(&[(0, 0), (0, 1), (0, 2)]);
|
||||||
|
|
||||||
assert_eq!(board.get(0, 0).1, Archetype::HCrate); // immobile
|
assert_eq!(board.get(0, 0).1, Archetype::Builtin(Builtin::HCrate, "hcrate")); // immobile
|
||||||
assert_eq!(board.get(0, 1).1, Archetype::Empty); // crate moved out
|
assert_eq!(board.get(0, 1).1, Archetype::Empty); // crate moved out
|
||||||
assert_eq!(board.get(0, 2).1, Archetype::Crate); // moved from (0,1)
|
assert_eq!(board.get(0, 2).1, Archetype::Builtin(Builtin::Crate, "crate")); // moved from (0,1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1261,8 +1258,8 @@ pub(crate) mod tests {
|
|||||||
let mut board = open_board(3, 1, (1, 0), vec![]);
|
let mut board = open_board(3, 1, (1, 0), vec![]);
|
||||||
board.dark = true;
|
board.dark = true;
|
||||||
let mut lamp = ObjectDef::new(1, 0);
|
let mut lamp = ObjectDef::new(1, 0);
|
||||||
lamp.solid = false;
|
lamp.behavior.solid = false;
|
||||||
lamp.light = 4;
|
lamp.behavior.glow = 4;
|
||||||
lamp.glyph = Glyph { tile: 1, fg: Rgba8 { r: 255, g: 0, b: 0, a: 255 }, bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 } };
|
lamp.glyph = Glyph { tile: 1, fg: Rgba8 { r: 255, g: 0, b: 0, a: 255 }, bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 } };
|
||||||
board.add_object(lamp);
|
board.add_object(lamp);
|
||||||
|
|
||||||
|
|||||||
+20
-20
@@ -313,7 +313,7 @@ impl GameState {
|
|||||||
}
|
}
|
||||||
Action::SetLight(radius) => {
|
Action::SetLight(radius) => {
|
||||||
if let Some(obj) = board.objects.get_mut(&ba.source) {
|
if let Some(obj) = board.objects.get_mut(&ba.source) {
|
||||||
obj.light = radius;
|
obj.behavior.glow = radius;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::SetTag {
|
Action::SetTag {
|
||||||
@@ -405,7 +405,7 @@ impl GameState {
|
|||||||
"teleport({target},{x},{y}): no such object"
|
"teleport({target},{x},{y}): no such object"
|
||||||
)),
|
)),
|
||||||
Some(obj) => {
|
Some(obj) => {
|
||||||
let source_solid = obj.solid;
|
let source_solid = obj.behavior.solid;
|
||||||
let (old_x, old_y) = (obj.x as i64, obj.y as i64);
|
let (old_x, old_y) = (obj.x as i64, obj.y as i64);
|
||||||
let blocked = source_solid
|
let blocked = source_solid
|
||||||
&& matches!(
|
&& matches!(
|
||||||
@@ -742,7 +742,7 @@ fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> StepOutcome {
|
|||||||
entered: Vec::new(),
|
entered: Vec::new(),
|
||||||
};
|
};
|
||||||
let (dx, dy): (i64, i64) = dir.into();
|
let (dx, dy): (i64, i64) = dir.into();
|
||||||
let Some((ox, oy, solid)) = board.objects.get(&id).map(|o| (o.x, o.y, o.solid)) else {
|
let Some((ox, oy, solid)) = board.objects.get(&id).map(|o| (o.x, o.y, o.behavior.solid)) else {
|
||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
let target = (ox as i64 + dx, oy as i64 + dy);
|
let target = (ox as i64 + dx, oy as i64 + dy);
|
||||||
@@ -805,7 +805,7 @@ mod tests {
|
|||||||
// ordinary solid here: the chain can't move (the player is backed against
|
// ordinary solid here: the chain can't move (the player is backed against
|
||||||
// the board edge), so nothing happens and the gem is not collected.
|
// the board edge), so nothing happens and the gem is not collected.
|
||||||
let mut sobj = ObjectDef::new(0, 0);
|
let mut sobj = ObjectDef::new(0, 0);
|
||||||
sobj.solid = false;
|
sobj.behavior.solid = false;
|
||||||
sobj.script_name = Some("s".to_string());
|
sobj.script_name = Some("s".to_string());
|
||||||
let mut board = open_board(3, 1, (2, 0), vec![sobj]);
|
let mut board = open_board(3, 1, (2, 0), vec![sobj]);
|
||||||
stamp(&mut board, 1, 0, Archetype::Builtin(Builtin::Gem, "gem"));
|
stamp(&mut board, 1, 0, Archetype::Builtin(Builtin::Gem, "gem"));
|
||||||
@@ -822,14 +822,14 @@ mod tests {
|
|||||||
|
|
||||||
// No grab: the gem is untouched and the player never moved.
|
// No grab: the gem is untouched and the player never moved.
|
||||||
assert_eq!(game.player.borrow().gems, 0);
|
assert_eq!(game.player.borrow().gems, 0);
|
||||||
assert!(game.board().objects.values().any(|o| o.grab));
|
assert!(game.board().objects.values().any(|o| o.behavior.grab));
|
||||||
assert_eq!((game.board().player.x, game.board().player.y), (2, 0));
|
assert_eq!((game.board().player.x, game.board().player.y), (2, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a `GameState` with one non-solid object running `src` as its script.
|
/// Builds a `GameState` with one non-solid object running `src` as its script.
|
||||||
fn game_with_object_script(board_w: usize, src: &str) -> GameState {
|
fn game_with_object_script(board_w: usize, src: &str) -> GameState {
|
||||||
let mut obj = ObjectDef::new(0, 0);
|
let mut obj = ObjectDef::new(0, 0);
|
||||||
obj.solid = false;
|
obj.behavior.solid = false;
|
||||||
obj.script_name = Some("s".to_string());
|
obj.script_name = Some("s".to_string());
|
||||||
let mut board = open_board(board_w, 1, (board_w as i64 - 1, 0), vec![obj]);
|
let mut board = open_board(board_w, 1, (board_w as i64 - 1, 0), vec![obj]);
|
||||||
crate_at(&mut board, 2, 0);
|
crate_at(&mut board, 2, 0);
|
||||||
@@ -850,7 +850,7 @@ mod tests {
|
|||||||
game.tick(Duration::from_millis(16));
|
game.tick(Duration::from_millis(16));
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!(b.get(2, 0).1, Archetype::Empty);
|
assert_eq!(b.get(2, 0).1, Archetype::Empty);
|
||||||
assert_eq!(b.get(3, 0).1, Archetype::Crate);
|
assert_eq!(b.get(3, 0).1, Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -859,7 +859,7 @@ mod tests {
|
|||||||
// object sits at (0,0); the player at (3,0). passable should report only the
|
// object sits at (0,0); the player at (3,0). passable should report only the
|
||||||
// genuinely empty in-bounds cell.
|
// genuinely empty in-bounds cell.
|
||||||
let mut obj = ObjectDef::new(0, 0);
|
let mut obj = ObjectDef::new(0, 0);
|
||||||
obj.solid = false;
|
obj.behavior.solid = false;
|
||||||
obj.script_name = Some("s".to_string());
|
obj.script_name = Some("s".to_string());
|
||||||
let mut board = open_board(4, 1, (3, 0), vec![obj]);
|
let mut board = open_board(4, 1, (3, 0), vec![obj]);
|
||||||
crate_at(&mut board, 2, 0);
|
crate_at(&mut board, 2, 0);
|
||||||
@@ -886,7 +886,7 @@ mod tests {
|
|||||||
// right after run_init (dt = 0) — under the old queued-Action behavior it
|
// right after run_init (dt = 0) — under the old queued-Action behavior it
|
||||||
// would have been stuck behind the delay and absent here.
|
// would have been stuck behind the delay and absent here.
|
||||||
let mut obj = ObjectDef::new(0, 0);
|
let mut obj = ObjectDef::new(0, 0);
|
||||||
obj.solid = false;
|
obj.behavior.solid = false;
|
||||||
obj.script_name = Some("s".to_string());
|
obj.script_name = Some("s".to_string());
|
||||||
let board = open_board(4, 1, (3, 0), vec![obj]);
|
let board = open_board(4, 1, (3, 0), vec![obj]);
|
||||||
let src = "fn init(m) { delay(5.0); log(\"immediate\"); }";
|
let src = "fn init(m) { delay(5.0); log(\"immediate\"); }";
|
||||||
@@ -916,7 +916,7 @@ mod tests {
|
|||||||
ccw: bool,
|
ccw: bool,
|
||||||
) -> GameState {
|
) -> GameState {
|
||||||
let mut obj = ObjectDef::new(1, 1);
|
let mut obj = ObjectDef::new(1, 1);
|
||||||
obj.solid = false;
|
obj.behavior.solid = false;
|
||||||
obj.script_name = Some("spinner".to_string());
|
obj.script_name = Some("spinner".to_string());
|
||||||
if ccw {
|
if ccw {
|
||||||
obj.tags.insert("BUILTIN_spinner_ccw".to_string());
|
obj.tags.insert("BUILTIN_spinner_ccw".to_string());
|
||||||
@@ -945,9 +945,9 @@ mod tests {
|
|||||||
let mut game = spinner_board(&[(1, 0), (2, 0)], &[(2, 1)]);
|
let mut game = spinner_board(&[(1, 0), (2, 0)], &[(2, 1)]);
|
||||||
game.tick(Duration::from_millis(16));
|
game.tick(Duration::from_millis(16));
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!(b.get(1, 0).1, Archetype::Crate); // N kept
|
assert_eq!(b.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // N kept
|
||||||
assert_eq!(b.get(2, 0).1, Archetype::Crate); // NE kept (not destroyed)
|
assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // NE kept (not destroyed)
|
||||||
assert_eq!(b.get(2, 1).1, Archetype::Wall); // wall kept
|
assert_eq!(b.get(2, 1).1, Archetype::Builtin(Builtin::Wall, "wall")); // wall kept
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -957,9 +957,9 @@ mod tests {
|
|||||||
let mut game = spinner_board(&[(0, 1), (0, 0), (1, 0)], &[]);
|
let mut game = spinner_board(&[(0, 1), (0, 0), (1, 0)], &[]);
|
||||||
game.tick(Duration::from_millis(16));
|
game.tick(Duration::from_millis(16));
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!(b.get(2, 0).1, Archetype::Crate); // NE: filled by N
|
assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // NE: filled by N
|
||||||
assert_eq!(b.get(1, 0).1, Archetype::Crate); // N: filled by NW
|
assert_eq!(b.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // N: filled by NW
|
||||||
assert_eq!(b.get(0, 0).1, Archetype::Crate); // NW: filled by W
|
assert_eq!(b.get(0, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // NW: filled by W
|
||||||
assert_eq!(b.get(0, 1).1, Archetype::Empty); // W: vacated (hole moved here)
|
assert_eq!(b.get(0, 1).1, Archetype::Empty); // W: vacated (hole moved here)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -970,7 +970,7 @@ mod tests {
|
|||||||
let mut game = spinner_board_dir(&[(1, 0)], &[], true);
|
let mut game = spinner_board_dir(&[(1, 0)], &[], true);
|
||||||
game.tick(Duration::from_millis(16));
|
game.tick(Duration::from_millis(16));
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!(b.get(0, 0).1, Archetype::Crate); // NW: crate rotated counter-clockwise
|
assert_eq!(b.get(0, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // NW: crate rotated counter-clockwise
|
||||||
assert_eq!(b.get(2, 0).1, Archetype::Empty); // NE: untouched
|
assert_eq!(b.get(2, 0).1, Archetype::Empty); // NE: untouched
|
||||||
assert_eq!(b.get(1, 0).1, Archetype::Empty); // N: vacated
|
assert_eq!(b.get(1, 0).1, Archetype::Empty); // N: vacated
|
||||||
}
|
}
|
||||||
@@ -999,7 +999,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn set_key_gives_and_takes_keys() {
|
fn set_key_gives_and_takes_keys() {
|
||||||
let mut sobj = ObjectDef::new(0, 0);
|
let mut sobj = ObjectDef::new(0, 0);
|
||||||
sobj.solid = false;
|
sobj.behavior.solid = false;
|
||||||
sobj.script_name = Some("s".to_string());
|
sobj.script_name = Some("s".to_string());
|
||||||
let board = open_board(2, 1, (1, 0), vec![sobj]);
|
let board = open_board(2, 1, (1, 0), vec![sobj]);
|
||||||
let scripts = HashMap::from([(
|
let scripts = HashMap::from([(
|
||||||
@@ -1016,7 +1016,7 @@ mod tests {
|
|||||||
|
|
||||||
// A second script can take a key.
|
// A second script can take a key.
|
||||||
let mut sobj2 = ObjectDef::new(0, 0);
|
let mut sobj2 = ObjectDef::new(0, 0);
|
||||||
sobj2.solid = false;
|
sobj2.behavior.solid = false;
|
||||||
sobj2.script_name = Some("t".to_string());
|
sobj2.script_name = Some("t".to_string());
|
||||||
let board2 = open_board(2, 1, (1, 0), vec![sobj2]);
|
let board2 = open_board(2, 1, (1, 0), vec![sobj2]);
|
||||||
let scripts2 = HashMap::from([(
|
let scripts2 = HashMap::from([(
|
||||||
@@ -1032,7 +1032,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn set_key_unknown_color_logs_error() {
|
fn set_key_unknown_color_logs_error() {
|
||||||
let mut sobj = ObjectDef::new(0, 0);
|
let mut sobj = ObjectDef::new(0, 0);
|
||||||
sobj.solid = false;
|
sobj.behavior.solid = false;
|
||||||
sobj.script_name = Some("s".to_string());
|
sobj.script_name = Some("s".to_string());
|
||||||
let board = open_board(2, 1, (1, 0), vec![sobj]);
|
let board = open_board(2, 1, (1, 0), vec![sobj]);
|
||||||
let scripts = HashMap::from([(
|
let scripts = HashMap::from([(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use color::Rgba8;
|
use color::Rgba8;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use rhai::Engine;
|
use rhai::Engine;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::script::Registerable;
|
use crate::script::Registerable;
|
||||||
use crate::utils::LogSink;
|
use crate::utils::LogSink;
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ use crate::utils::LogSink;
|
|||||||
/// `Glyph` values come from the map file palette and are set at load time.
|
/// `Glyph` values come from the map file palette and are set at load time.
|
||||||
/// The player is the only entity whose glyph is hardcoded at runtime
|
/// The player is the only entity whose glyph is hardcoded at runtime
|
||||||
/// (see [`Glyph::player`]).
|
/// (see [`Glyph::player`]).
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||||
pub struct Glyph {
|
pub struct Glyph {
|
||||||
/// Tile index into the board's bitmap font (left-to-right, top-to-bottom).
|
/// Tile index into the board's bitmap font (left-to-right, top-to-bottom).
|
||||||
pub tile: u32,
|
pub tile: u32,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use crate::map_file::{TileIndex, parse_color};
|
|||||||
use crate::object_def::ObjectDef;
|
use crate::object_def::ObjectDef;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use crate::utils::Pushable;
|
||||||
|
|
||||||
/// Serde representation of the board `[grid]`: a char grid plus its palette.
|
/// Serde representation of the board `[grid]`: a char grid plus its palette.
|
||||||
///
|
///
|
||||||
@@ -84,7 +85,7 @@ pub(crate) struct PaletteEntry {
|
|||||||
pub opaque: Option<bool>,
|
pub opaque: Option<bool>,
|
||||||
/// Object pushability (defaults `false`). Only for `kind = "object"`.
|
/// Object pushability (defaults `false`). Only for `kind = "object"`.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub pushable: Option<bool>,
|
pub pushable: Option<Pushable>,
|
||||||
/// Light radius in cells emitted on a dark board (defaults `0` = none).
|
/// Light radius in cells emitted on a dark board (defaults `0` = none).
|
||||||
/// Only for `kind = "object"`. See [`ObjectDef::light`].
|
/// Only for `kind = "object"`. See [`ObjectDef::light`].
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
@@ -129,7 +130,7 @@ pub(crate) struct ObjectTemplate {
|
|||||||
pub glyph: Glyph,
|
pub glyph: Glyph,
|
||||||
pub solid: bool,
|
pub solid: bool,
|
||||||
pub opaque: bool,
|
pub opaque: bool,
|
||||||
pub pushable: bool,
|
pub pushable: Pushable,
|
||||||
/// Light radius in cells (0 = none); see [`ObjectDef::light`].
|
/// Light radius in cells (0 = none); see [`ObjectDef::light`].
|
||||||
pub light: u32,
|
pub light: u32,
|
||||||
pub script_name: Option<String>,
|
pub script_name: Option<String>,
|
||||||
@@ -179,7 +180,7 @@ fn resolve_entry(ch: char, e: &PaletteEntry, errors: &mut Vec<LogLine>) -> Resol
|
|||||||
glyph: glyph_with_default(ObjectDef::default_glyph()),
|
glyph: glyph_with_default(ObjectDef::default_glyph()),
|
||||||
solid: e.solid.unwrap_or(true),
|
solid: e.solid.unwrap_or(true),
|
||||||
opaque: e.opaque.unwrap_or(true),
|
opaque: e.opaque.unwrap_or(true),
|
||||||
pushable: e.pushable.unwrap_or(false),
|
pushable: e.pushable.unwrap_or(Pushable::No),
|
||||||
light: e.light.unwrap_or(0),
|
light: e.light.unwrap_or(0),
|
||||||
script_name: e.script_name.clone(),
|
script_name: e.script_name.clone(),
|
||||||
tags: e.tags.clone().unwrap_or_default(),
|
tags: e.tags.clone().unwrap_or_default(),
|
||||||
|
|||||||
@@ -34,3 +34,4 @@ pub use utils::Direction;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
mod api;
|
mod api;
|
||||||
|
pub mod tile;
|
||||||
|
|||||||
+24
-18
@@ -25,7 +25,7 @@ use crate::glyph::Glyph;
|
|||||||
use crate::layer::{GridData, PaletteEntry, Placement, build_grid};
|
use crate::layer::{GridData, PaletteEntry, Placement, build_grid};
|
||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
use crate::object_def::ObjectDef;
|
use crate::object_def::ObjectDef;
|
||||||
use crate::utils::{ObjectId, PlayerPos, PortalDef};
|
use crate::utils::{Behavior, ObjectId, PlayerPos, PortalDef, Pushable};
|
||||||
|
|
||||||
/// The serde shell for one board in a `.toml` file: a header, the single grid, and
|
/// The serde shell for one board in a `.toml` file: a header, the single grid, and
|
||||||
/// the off-grid trigger/decoration lists.
|
/// the off-grid trigger/decoration lists.
|
||||||
@@ -316,13 +316,16 @@ impl TryFrom<MapFile> for Board {
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
glyph: t.glyph,
|
glyph: t.glyph,
|
||||||
solid: t.solid,
|
behavior: Behavior {
|
||||||
opaque: t.opaque,
|
solid: t.solid,
|
||||||
pushable: t.pushable,
|
opaque: t.opaque,
|
||||||
light: t.light,
|
pushable: t.pushable,
|
||||||
// Hand-placed objects are never grab targets; only expanded
|
glow: t.light,
|
||||||
// grab archetypes (e.g. gems) set this (see expand_builtin_archetypes).
|
// Hand-placed objects are never grab targets; only expanded
|
||||||
grab: false,
|
// grab archetypes (e.g. gems) set this (see expand_builtin_archetypes).
|
||||||
|
// TODO this is wrong, obviously we want grabbable objects
|
||||||
|
grab: false,
|
||||||
|
},
|
||||||
script_name: t.script_name,
|
script_name: t.script_name,
|
||||||
// Script-backed archetypes are expanded after the board is built
|
// Script-backed archetypes are expanded after the board is built
|
||||||
// (see `expand_builtin_archetypes` below), not via templates.
|
// (see `expand_builtin_archetypes` below), not via templates.
|
||||||
@@ -355,11 +358,13 @@ impl TryFrom<MapFile> for Board {
|
|||||||
x: t.x,
|
x: t.x,
|
||||||
y: t.y,
|
y: t.y,
|
||||||
glyph: Glyph::transparent(),
|
glyph: Glyph::transparent(),
|
||||||
solid: false,
|
behavior: Behavior {
|
||||||
opaque: false,
|
solid: false,
|
||||||
pushable: false,
|
opaque: false,
|
||||||
light: 0,
|
pushable: Pushable::No,
|
||||||
grab: false,
|
glow: 0,
|
||||||
|
grab: false,
|
||||||
|
},
|
||||||
script_name: Some(t.script_name),
|
script_name: Some(t.script_name),
|
||||||
builtin_script: None,
|
builtin_script: None,
|
||||||
tags: t.tags.unwrap_or_default().into_iter().collect(),
|
tags: t.tags.unwrap_or_default().into_iter().collect(),
|
||||||
@@ -406,6 +411,7 @@ impl TryFrom<MapFile> for Board {
|
|||||||
grid,
|
grid,
|
||||||
floor,
|
floor,
|
||||||
decorations,
|
decorations,
|
||||||
|
sensors: Vec::new(),
|
||||||
player: PlayerPos {
|
player: PlayerPos {
|
||||||
x: px as i64,
|
x: px as i64,
|
||||||
y: py as i64,
|
y: py as i64,
|
||||||
@@ -514,7 +520,7 @@ fn cell_entry(glyph: Glyph, arch: Archetype) -> PaletteEntry {
|
|||||||
/// Whether an object is a **trigger** — an invisible, non-solid, script-only object
|
/// Whether an object is a **trigger** — an invisible, non-solid, script-only object
|
||||||
/// authored/serialized in `[[triggers]]` rather than the grid palette.
|
/// authored/serialized in `[[triggers]]` rather than the grid palette.
|
||||||
fn is_trigger(o: &ObjectDef) -> bool {
|
fn is_trigger(o: &ObjectDef) -> bool {
|
||||||
!o.solid && o.glyph.tile == 0 && o.script_name.is_some() && o.builtin_script.is_none()
|
!o.behavior.solid && o.glyph.tile == 0 && o.script_name.is_some() && o.builtin_script.is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serializes `board`'s single grid (terrain + non-trigger objects + portals +
|
/// Serializes `board`'s single grid (terrain + non-trigger objects + portals +
|
||||||
@@ -563,10 +569,10 @@ fn grid_to_data(board: &Board) -> GridData {
|
|||||||
tile: Some(TileIndex::Num(o.glyph.tile)),
|
tile: Some(TileIndex::Num(o.glyph.tile)),
|
||||||
fg: Some(color_to_hex(o.glyph.fg)),
|
fg: Some(color_to_hex(o.glyph.fg)),
|
||||||
bg: Some(color_to_hex(o.glyph.bg)),
|
bg: Some(color_to_hex(o.glyph.bg)),
|
||||||
solid: Some(o.solid),
|
solid: Some(o.behavior.solid),
|
||||||
opaque: Some(o.opaque),
|
opaque: Some(o.behavior.opaque),
|
||||||
pushable: Some(o.pushable),
|
pushable: Some(o.behavior.pushable),
|
||||||
light: (o.light > 0).then_some(o.light),
|
light: (o.behavior.glow > 0).then_some(o.behavior.glow),
|
||||||
script_name: o.script_name.clone(),
|
script_name: o.script_name.clone(),
|
||||||
tags: (!tags.is_empty()).then_some(tags),
|
tags: (!tags.is_empty()).then_some(tags),
|
||||||
name: o.name.clone(),
|
name: o.name.clone(),
|
||||||
|
|||||||
+10
-25
@@ -1,5 +1,5 @@
|
|||||||
use crate::glyph::Glyph;
|
use crate::glyph::Glyph;
|
||||||
use crate::utils::ObjectId;
|
use crate::utils::{Behavior, ObjectId, Pushable};
|
||||||
use color::Rgba8;
|
use color::Rgba8;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use crate::api::queue::ObjQueue;
|
use crate::api::queue::ObjQueue;
|
||||||
@@ -37,25 +37,8 @@ pub struct ObjectDef {
|
|||||||
/// Visual representation of this object. Owned by the object (not derived
|
/// Visual representation of this object. Owned by the object (not derived
|
||||||
/// from the grid cell), so scripts can change tile, fg, and bg at runtime.
|
/// from the grid cell), so scripts can change tile, fg, and bg at runtime.
|
||||||
pub glyph: Glyph,
|
pub glyph: Glyph,
|
||||||
/// Whether the object blocks / participates in movement. A solid object stops
|
/// All the normal `Behavior` things about this object
|
||||||
/// a mover walking into it; at most one solid (object or grid archetype) may
|
pub behavior: Behavior,
|
||||||
/// occupy a cell. Inverse of the old `passable` flag.
|
|
||||||
pub solid: bool,
|
|
||||||
/// Whether the object blocks line of sight / FOV for the player
|
|
||||||
pub opaque: bool,
|
|
||||||
/// Whether the object can be shoved by a mover. Unused for now (no push
|
|
||||||
/// mechanic yet); defaults to `false`.
|
|
||||||
pub pushable: bool,
|
|
||||||
/// Whether walking into this object **grabs** it: the player moves onto the
|
|
||||||
/// cell and the object's `grab()` hook fires (the object is expected to
|
|
||||||
/// remove itself via `die()`). Set when a grab archetype (e.g. a gem) is
|
|
||||||
/// expanded into an object. Defaults to `false`.
|
|
||||||
pub grab: bool,
|
|
||||||
/// Light radius in cells that this object emits on a `dark` board (0 = no
|
|
||||||
/// light). The emitted *color* is this object's own glyph foreground color,
|
|
||||||
/// so a red glyph casts red light. See [`crate::fov`] for the lighting model.
|
|
||||||
/// Scripts can change it at runtime via `set_light(n)`. Defaults to `0`.
|
|
||||||
pub light: u32,
|
|
||||||
/// Compile-key of the Rhai script that drives this object: a name in
|
/// Compile-key of the Rhai script that drives this object: a name in
|
||||||
/// [`World::scripts`](crate::world::World::scripts) for a hand-authored object,
|
/// [`World::scripts`](crate::world::World::scripts) for a hand-authored object,
|
||||||
/// or a synthetic `BUILTIN_*` name set when a script-backed archetype is
|
/// or a synthetic `BUILTIN_*` name set when a script-backed archetype is
|
||||||
@@ -103,11 +86,13 @@ impl ObjectDef {
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
glyph: Self::default_glyph(),
|
glyph: Self::default_glyph(),
|
||||||
solid: true,
|
behavior: Behavior {
|
||||||
opaque: true,
|
solid: true,
|
||||||
pushable: false,
|
opaque: true,
|
||||||
grab: false,
|
pushable: Pushable::No,
|
||||||
light: 0,
|
grab: false,
|
||||||
|
glow: 0,
|
||||||
|
},
|
||||||
script_name: None,
|
script_name: None,
|
||||||
builtin_script: None,
|
builtin_script: None,
|
||||||
tags: HashSet::new(),
|
tags: HashSet::new(),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::archetype::Archetype;
|
|||||||
use crate::board::tests::{crate_at, open_board, wall_at};
|
use crate::board::tests::{crate_at, open_board, wall_at};
|
||||||
use crate::game::GameState;
|
use crate::game::GameState;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use crate::Builtin;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn move_command_relocates_the_source_object() {
|
fn move_command_relocates_the_source_object() {
|
||||||
@@ -45,7 +46,7 @@ fn object_pushes_crate_on_init() {
|
|||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0));
|
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0));
|
||||||
assert_eq!(b.get(2, 0).1, Archetype::Empty);
|
assert_eq!(b.get(2, 0).1, Archetype::Empty);
|
||||||
assert_eq!(b.get(3, 0).1, Archetype::Crate);
|
assert_eq!(b.get(3, 0).1, Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ fn make_board(px: i64, py: i64, portals: Vec<PortalDef>) -> Board {
|
|||||||
grid: vec![(Glyph::transparent(), Archetype::Empty); 9],
|
grid: vec![(Glyph::transparent(), Archetype::Empty); 9],
|
||||||
floor: Floor::Blank,
|
floor: Floor::Blank,
|
||||||
decorations: Vec::new(),
|
decorations: Vec::new(),
|
||||||
|
sensors: Vec::new(),
|
||||||
player: PlayerPos { x: px, y: py },
|
player: PlayerPos { x: px, y: py },
|
||||||
objects: BTreeMap::new(),
|
objects: BTreeMap::new(),
|
||||||
next_object_id: 1,
|
next_object_id: 1,
|
||||||
|
|||||||
@@ -19,12 +19,14 @@ palette = { "#" = { kind = "wall", tile = 35, fg = "#808080", bg = "#606060" } }
|
|||||||
let board = load_board(toml);
|
let board = load_board(toml);
|
||||||
for y in 0..2 {
|
for y in 0..2 {
|
||||||
for x in 0..3 {
|
for x in 0..3 {
|
||||||
let expected = if (x, y) == (0, 0) {
|
if (x, y) == (0, 0) { // player won its fallback cell
|
||||||
Archetype::Empty // player won its fallback cell
|
assert!(board.solid_at(x, y).unwrap().player());
|
||||||
} else {
|
} else {
|
||||||
Archetype::Wall
|
let obj = &board.objects[&board.object_ids_at(x, y)[0]];
|
||||||
};
|
let tag = obj.tags.iter().next().unwrap();
|
||||||
assert_eq!(board.get(x, y).1, expected, "cell ({x}, {y})");
|
assert_eq!(tag, "BUILTIN_wall")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert_eq!((board.player.x, board.player.y), (0, 0));
|
assert_eq!((board.player.x, board.player.y), (0, 0));
|
||||||
|
|||||||
@@ -77,6 +77,6 @@ fn non_solid_object_and_wall_coexist_in_separate_cells() {
|
|||||||
// never be authored onto a wall cell. A wall and a (separate-cell) non-solid
|
// never be authored onto a wall cell. A wall and a (separate-cell) non-solid
|
||||||
// object both load fine.
|
// object both load fine.
|
||||||
let board = load_board(&map_3x1_object("#G@", "G", "solid = false"));
|
let board = load_board(&map_3x1_object("#G@", "G", "solid = false"));
|
||||||
assert_eq!(board.objects.len(), 1);
|
assert_eq!(board.objects.len(), 2); // The wall (for now) shows up as an object because it's a builtin, TODO
|
||||||
assert!(board.is_valid());
|
assert!(board.is_valid());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use crate::game::GameState;
|
|||||||
use crate::map_file::MapFile;
|
use crate::map_file::MapFile;
|
||||||
use crate::object_def::ObjectDef;
|
use crate::object_def::ObjectDef;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use crate::{Board, Builtin};
|
||||||
|
|
||||||
/// Finds the pusher object on a board (by its built-in tag).
|
/// Finds the pusher object on a board (by its built-in tag).
|
||||||
fn pusher<'a>(board: &'a crate::board::Board, id: &mut u32) -> &'a ObjectDef {
|
fn pusher<'a>(board: &'a crate::board::Board, id: &mut u32) -> &'a ObjectDef {
|
||||||
@@ -42,6 +43,10 @@ fn pusher_loads_as_a_tagged_scripted_solid_object() {
|
|||||||
assert!(!board.is_passable(0, 0), "pusher is solid");
|
assert!(!board.is_passable(0, 0), "pusher is solid");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_tag(board: &Board, x: usize, y: usize, tag: &str) -> bool {
|
||||||
|
board.object_ids_at(x, y).iter().any(|id| board.objects[id].tags.contains(tag))
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pusher_advances_and_shoves_a_crate() {
|
fn pusher_advances_and_shoves_a_crate() {
|
||||||
// Pusher at (0,0), crate at (1,0), the player parked at the east edge.
|
// Pusher at (0,0), crate at (1,0), the player parked at the east edge.
|
||||||
@@ -74,7 +79,7 @@ fn pusher_advances_and_shoves_a_crate() {
|
|||||||
"crate left its start cell"
|
"crate left its start cell"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
(2..b.width).any(|x| b.get(x, 0).1 == Archetype::Crate),
|
(2..b.width).any(|x| is_tag(&b, x, 0, "BUILTIN_crate")),
|
||||||
"crate was shoved east"
|
"crate was shoved east"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use crate::utils::Direction;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transporter_loads_as_a_tagged_scripted_solid_object() {
|
fn transporter_loads_as_a_tagged_scripted_object() {
|
||||||
let board = load_board(&map(
|
let board = load_board(&map(
|
||||||
3,
|
3,
|
||||||
1,
|
1,
|
||||||
@@ -26,9 +26,6 @@ fn transporter_loads_as_a_tagged_scripted_solid_object() {
|
|||||||
.expect("transporter object");
|
.expect("transporter object");
|
||||||
assert_eq!((obj.x, obj.y), (1, 0));
|
assert_eq!((obj.x, obj.y), (1, 0));
|
||||||
assert!(obj.builtin_script.is_some(), "carries the embedded script");
|
assert!(obj.builtin_script.is_some(), "carries the embedded script");
|
||||||
assert!(obj.solid, "transporter is solid");
|
|
||||||
assert!(!obj.opaque, "transporter is see-through");
|
|
||||||
assert!(!obj.pushable, "transporter cannot be pushed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ fn board_with_object(
|
|||||||
grid: vec![(Glyph::transparent(), Archetype::Empty)],
|
grid: vec![(Glyph::transparent(), Archetype::Empty)],
|
||||||
floor: Floor::Blank,
|
floor: Floor::Blank,
|
||||||
decorations: Vec::new(),
|
decorations: Vec::new(),
|
||||||
|
sensors: Vec::new(),
|
||||||
player: PlayerPos { x: 0, y: 0 },
|
player: PlayerPos { x: 0, y: 0 },
|
||||||
objects: BTreeMap::from([(1, object)]),
|
objects: BTreeMap::from([(1, object)]),
|
||||||
next_object_id: 2,
|
next_object_id: 2,
|
||||||
@@ -64,7 +65,7 @@ fn scripted_object(x: usize, y: usize, script: &str) -> ObjectDef {
|
|||||||
/// kind that receives an `enter` hook when a solid relocates onto its cell.
|
/// kind that receives an `enter` hook when a solid relocates onto its cell.
|
||||||
fn nonsolid_object(x: usize, y: usize, script: &str) -> ObjectDef {
|
fn nonsolid_object(x: usize, y: usize, script: &str) -> ObjectDef {
|
||||||
let mut o = scripted_object(x, y, script);
|
let mut o = scripted_object(x, y, script);
|
||||||
o.solid = false;
|
o.behavior.solid = false;
|
||||||
o
|
o
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ use crate::board::tests::{add_floor, crate_at, open_board, stamp, wall_at};
|
|||||||
use crate::game::GameState;
|
use crate::game::GameState;
|
||||||
use crate::glyph::Glyph;
|
use crate::glyph::Glyph;
|
||||||
use crate::object_def::ObjectDef;
|
use crate::object_def::ObjectDef;
|
||||||
use crate::utils::Direction;
|
use crate::utils::{Direction, Pushable};
|
||||||
use color::Rgba8;
|
use color::Rgba8;
|
||||||
|
use crate::Builtin;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pushing_a_crate_reveals_the_floor_underneath() {
|
fn pushing_a_crate_reveals_the_floor_underneath() {
|
||||||
@@ -32,7 +33,7 @@ fn pushing_a_crate_reveals_the_floor_underneath() {
|
|||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.try_move(Direction::East);
|
game.try_move(Direction::East);
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!(b.get(2, 0).1, Archetype::Crate);
|
assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
assert_eq!(b.get(1, 0).1, Archetype::Empty);
|
assert_eq!(b.get(1, 0).1, Archetype::Empty);
|
||||||
assert_eq!(b.glyph_at(0, 0), floor_glyph);
|
assert_eq!(b.glyph_at(0, 0), floor_glyph);
|
||||||
}
|
}
|
||||||
@@ -46,7 +47,7 @@ fn player_pushes_single_crate() {
|
|||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.player.x, b.player.y), (1, 0));
|
assert_eq!((b.player.x, b.player.y), (1, 0));
|
||||||
assert_eq!(b.get(1, 0).1, Archetype::Empty);
|
assert_eq!(b.get(1, 0).1, Archetype::Empty);
|
||||||
assert_eq!(b.get(2, 0).1, Archetype::Crate);
|
assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -58,8 +59,8 @@ fn push_blocked_by_wall_moves_nothing() {
|
|||||||
game.try_move(Direction::East);
|
game.try_move(Direction::East);
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.player.x, b.player.y), (0, 0));
|
assert_eq!((b.player.x, b.player.y), (0, 0));
|
||||||
assert_eq!(b.get(1, 0).1, Archetype::Crate);
|
assert_eq!(b.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
assert_eq!(b.get(2, 0).1, Archetype::Wall);
|
assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::Wall, "wall"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -71,7 +72,7 @@ fn push_blocked_by_edge_moves_nothing() {
|
|||||||
game.try_move(Direction::East);
|
game.try_move(Direction::East);
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.player.x, b.player.y), (0, 0));
|
assert_eq!((b.player.x, b.player.y), (0, 0));
|
||||||
assert_eq!(b.get(1, 0).1, Archetype::Crate);
|
assert_eq!(b.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -84,8 +85,8 @@ fn cascade_pushes_two_crates() {
|
|||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.player.x, b.player.y), (1, 0));
|
assert_eq!((b.player.x, b.player.y), (1, 0));
|
||||||
assert_eq!(b.get(1, 0).1, Archetype::Empty);
|
assert_eq!(b.get(1, 0).1, Archetype::Empty);
|
||||||
assert_eq!(b.get(2, 0).1, Archetype::Crate);
|
assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
assert_eq!(b.get(3, 0).1, Archetype::Crate);
|
assert_eq!(b.get(3, 0).1, Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -98,15 +99,15 @@ fn cascade_blocked_by_wall_moves_nothing() {
|
|||||||
game.try_move(Direction::East);
|
game.try_move(Direction::East);
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.player.x, b.player.y), (0, 0));
|
assert_eq!((b.player.x, b.player.y), (0, 0));
|
||||||
assert_eq!(b.get(1, 0).1, Archetype::Crate);
|
assert_eq!(b.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
assert_eq!(b.get(2, 0).1, Archetype::Crate);
|
assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn player_pushes_pushable_solid_object() {
|
fn player_pushes_pushable_solid_object() {
|
||||||
// A solid, pushable object is shoved exactly like a crate.
|
// A solid, pushable object is shoved exactly like a crate.
|
||||||
let mut obj = ObjectDef::new(1, 0);
|
let mut obj = ObjectDef::new(1, 0);
|
||||||
obj.pushable = true;
|
obj.behavior.pushable = Pushable::Any;
|
||||||
let board = open_board(4, 1, (0, 0), vec![obj]);
|
let board = open_board(4, 1, (0, 0), vec![obj]);
|
||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.try_move(Direction::East);
|
game.try_move(Direction::East);
|
||||||
@@ -119,44 +120,44 @@ fn player_pushes_pushable_solid_object() {
|
|||||||
fn hcrate_pushes_east_but_not_north() {
|
fn hcrate_pushes_east_but_not_north() {
|
||||||
// Pushing east: the HCrate slides.
|
// Pushing east: the HCrate slides.
|
||||||
let mut board = open_board(4, 1, (0, 0), vec![]);
|
let mut board = open_board(4, 1, (0, 0), vec![]);
|
||||||
stamp(&mut board, 1, 0, Archetype::HCrate);
|
stamp(&mut board, 1, 0, Archetype::Builtin(Builtin::HCrate, "hcrate"));
|
||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.try_move(Direction::East);
|
game.try_move(Direction::East);
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.player.x, b.player.y), (1, 0));
|
assert_eq!((b.player.x, b.player.y), (1, 0));
|
||||||
assert_eq!(b.get(1, 0).1, Archetype::Empty);
|
assert_eq!(b.get(1, 0).1, Archetype::Empty);
|
||||||
assert_eq!(b.get(2, 0).1, Archetype::HCrate);
|
assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::HCrate, "hcrate"));
|
||||||
drop(b);
|
drop(b);
|
||||||
|
|
||||||
// Pushing north into an HCrate: blocked, nothing moves.
|
// Pushing north into an HCrate: blocked, nothing moves.
|
||||||
let mut board = open_board(1, 4, (0, 3), vec![]);
|
let mut board = open_board(1, 4, (0, 3), vec![]);
|
||||||
stamp(&mut board, 0, 2, Archetype::HCrate);
|
stamp(&mut board, 0, 2, Archetype::Builtin(Builtin::HCrate, "hcrate"));
|
||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.try_move(Direction::North);
|
game.try_move(Direction::North);
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.player.x, b.player.y), (0, 3));
|
assert_eq!((b.player.x, b.player.y), (0, 3));
|
||||||
assert_eq!(b.get(0, 2).1, Archetype::HCrate);
|
assert_eq!(b.get(0, 2).1, Archetype::Builtin(Builtin::HCrate, "hcrate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vcrate_pushes_north_but_not_east() {
|
fn vcrate_pushes_north_but_not_east() {
|
||||||
// Pushing north: the VCrate slides.
|
// Pushing north: the VCrate slides.
|
||||||
let mut board = open_board(1, 4, (0, 3), vec![]);
|
let mut board = open_board(1, 4, (0, 3), vec![]);
|
||||||
stamp(&mut board, 0, 2, Archetype::VCrate);
|
stamp(&mut board, 0, 2, Archetype::Builtin(Builtin::VCrate, "vcrate"));
|
||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.try_move(Direction::North);
|
game.try_move(Direction::North);
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.player.x, b.player.y), (0, 2));
|
assert_eq!((b.player.x, b.player.y), (0, 2));
|
||||||
assert_eq!(b.get(0, 2).1, Archetype::Empty);
|
assert_eq!(b.get(0, 2).1, Archetype::Empty);
|
||||||
assert_eq!(b.get(0, 1).1, Archetype::VCrate);
|
assert_eq!(b.get(0, 1).1, Archetype::Builtin(Builtin::VCrate, "vcrate"));
|
||||||
drop(b);
|
drop(b);
|
||||||
|
|
||||||
// Pushing east into a VCrate: blocked, nothing moves.
|
// Pushing east into a VCrate: blocked, nothing moves.
|
||||||
let mut board = open_board(4, 1, (0, 0), vec![]);
|
let mut board = open_board(4, 1, (0, 0), vec![]);
|
||||||
stamp(&mut board, 1, 0, Archetype::VCrate);
|
stamp(&mut board, 1, 0, Archetype::Builtin(Builtin::VCrate, "vcrate"));
|
||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.try_move(Direction::East);
|
game.try_move(Direction::East);
|
||||||
let b = game.board();
|
let b = game.board();
|
||||||
assert_eq!((b.player.x, b.player.y), (0, 0));
|
assert_eq!((b.player.x, b.player.y), (0, 0));
|
||||||
assert_eq!(b.get(1, 0).1, Archetype::VCrate);
|
assert_eq!(b.get(1, 0).1, Archetype::Builtin(Builtin::VCrate, "vcrate"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::api::queue::ObjQueue;
|
||||||
|
use crate::glyph::Glyph;
|
||||||
|
use crate::object_def::ObjectDef;
|
||||||
|
use crate::utils::{Behavior, ObjectId, Pushable};
|
||||||
|
|
||||||
|
/// The various ways that a tile might respond to another tile trying to move on top of it
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Copy, Clone, Debug)]
|
||||||
|
pub enum EnterResponse {
|
||||||
|
/// Flat denial: don't let the move happen, block it.
|
||||||
|
Block,
|
||||||
|
/// Call the `grab` hook and then remove this object.
|
||||||
|
/// TODO will do something different once inventory exists as a concept
|
||||||
|
Grab,
|
||||||
|
/// Attempt to exit the cell ourselves, the opposite direction. This of course recurses; if our move
|
||||||
|
/// is denied then this is the same as a block.
|
||||||
|
Push(Pushable),
|
||||||
|
/// Call an `enter` hook and do something. The hook is responsible for resolving the conflict:
|
||||||
|
/// placing the moving-object somewhere else, moving ourselves somewhere else, or whatever.
|
||||||
|
/// It should return `true` or `false`: this return value is passed back up a `Push` chain to
|
||||||
|
/// the original moving object, determining whether the moves should happen.
|
||||||
|
/// Example: a teleporter moves anything that enters it to another spot, if it's unblocked. It
|
||||||
|
/// will check if the target cell is open and move the object there if it is, returning true.
|
||||||
|
/// If you push a crate into it, it returning true means you should move (into the space the crate
|
||||||
|
/// left behind); it returning false means the crate wasn't teleported so your move is blocked
|
||||||
|
/// also.
|
||||||
|
/// The hook return value is only used for _other_ things in the chain: regardless of what's returned,
|
||||||
|
/// the engine won't touch the crate or the teleporter; the hook is responsible for handling the
|
||||||
|
/// actual collision. A teleporter that teleports the crate and returns false will leave an empty
|
||||||
|
/// space behind (the player won't move into it because it returned false); a teleporter that
|
||||||
|
/// returns true without teleporting the crate, the crate will be destroyed by the player moving
|
||||||
|
/// on top of it.
|
||||||
|
Hook,
|
||||||
|
/// Swap places with whatever moved on top of you
|
||||||
|
Swap,
|
||||||
|
/// Get overwritten and destroyed by whatever moved on top of us. Right now, equivalent to `Grab`
|
||||||
|
/// if the hook does nothing (but in the future `Grab` will have other builtin behavior)
|
||||||
|
Squish
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Behavior> for EnterResponse {
|
||||||
|
fn from(behavior: Behavior) -> EnterResponse {
|
||||||
|
if behavior.grab {
|
||||||
|
EnterResponse::Grab
|
||||||
|
} else if behavior.pushable != Pushable::No {
|
||||||
|
EnterResponse::Push(behavior.pushable)
|
||||||
|
} else if behavior.solid {
|
||||||
|
EnterResponse::Block
|
||||||
|
} else {
|
||||||
|
EnterResponse::Squish
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where `Sensor`s are drawn, in relation to the grid:
|
||||||
|
/// - `Above` is above everything, including the player. If the glyph has a nonzero tile, and it's
|
||||||
|
/// visible / lit, it will be drawn.
|
||||||
|
/// - `Below` is below the grid but above the floor. A nonzero-tile-glyph will be drawn only if
|
||||||
|
/// there's not a grid-thing in the same cell.
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Copy, Clone, Debug)]
|
||||||
|
#[serde(rename = "lowercase")]
|
||||||
|
pub enum DrawLayer { Above, Below }
|
||||||
|
|
||||||
|
/// How a thing interacts with the lighting and visibility models
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Copy, Clone, Debug)]
|
||||||
|
#[serde(rename = "lowercase")]
|
||||||
|
pub struct Optics {
|
||||||
|
/// Opaque things block field of view
|
||||||
|
pub opaque: bool,
|
||||||
|
/// The brightness (in number of cells' radius) of light this
|
||||||
|
/// emits. It will emit light in the color of the foreground of its glyph.
|
||||||
|
/// A cell is visible if:
|
||||||
|
/// - the level isn't dark, in which case lighting isn't calculated
|
||||||
|
/// - the cell is within the field of view
|
||||||
|
/// - the cell is within the glow radius of at least one light source
|
||||||
|
///
|
||||||
|
/// Its color will be tinted by the light sources illuminating it.
|
||||||
|
pub glow: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Everything an object-or-sensor needs to have a Rhai script attached.
|
||||||
|
/// TODO clean this up some, especially the script_name-vs-builtin_script dichotomy
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct Scripting {
|
||||||
|
/// Compile-key of the Rhai script that drives this object: a name in
|
||||||
|
/// [`World::scripts`](crate::world::World::scripts) for a hand-authored object,
|
||||||
|
/// or a synthetic `BUILTIN_*` name set when a script-backed archetype is
|
||||||
|
/// expanded (see [`builtin_script`](ObjectDef::builtin_script)). `None` means
|
||||||
|
/// this object has no script yet.
|
||||||
|
pub script_name: Option<String>,
|
||||||
|
/// Embedded built-in script source, set when a script-backed archetype (e.g. a
|
||||||
|
/// `pusher_*` or `gem`) is expanded into an object at load time (see
|
||||||
|
/// [`crate::builtin_scripts`]). When set, this is the object's script *source*
|
||||||
|
/// (its compile-key is the synthetic `BUILTIN_*` [`script_name`](ObjectDef::script_name)
|
||||||
|
/// the same expansion assigns). Not part of the map file — it is regenerated
|
||||||
|
/// from the archetype on load.
|
||||||
|
pub builtin_script: Option<&'static str>,
|
||||||
|
/// Open-ended string labels for this object. Serialized as a TOML array;
|
||||||
|
/// not subject to any rate limit — mutations take effect immediately after
|
||||||
|
/// the frame's action queue is drained.
|
||||||
|
pub tags: HashSet<String>,
|
||||||
|
/// Optional unique human-readable name for this object. `None` if unnamed.
|
||||||
|
/// Names are validated for uniqueness at map-load time; a duplicate name is
|
||||||
|
/// cleared to `None` (the object survives but becomes anonymous).
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// The output queue of actions for this object
|
||||||
|
pub queue: ObjQueue,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `Sensor` is some kind of object that exists alongside the map: it's drawn on the board but can't
|
||||||
|
/// affect board movement, as it doesn't live in the grid.
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Sensor {
|
||||||
|
/// The unique ID of this sensor
|
||||||
|
pub id: ObjectId,
|
||||||
|
/// Where it is on the board: x coord
|
||||||
|
pub x: usize,
|
||||||
|
/// Where it is on the board: y coord
|
||||||
|
pub y: usize,
|
||||||
|
/// What it looks like
|
||||||
|
pub glyph: Glyph,
|
||||||
|
/// Sensors interact with FOV and lighting
|
||||||
|
pub optics: Optics,
|
||||||
|
/// Sensors have scripting ability
|
||||||
|
#[serde(skip)]
|
||||||
|
pub scripting: Scripting,
|
||||||
|
}
|
||||||
+15
-4
@@ -5,15 +5,18 @@ use crate::archetype::Archetype;
|
|||||||
use crate::glyph::Glyph;
|
use crate::glyph::Glyph;
|
||||||
use color::Rgba8;
|
use color::Rgba8;
|
||||||
use rhai::Dynamic;
|
use rhai::Dynamic;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::Board;
|
use crate::Board;
|
||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
|
use crate::tile::EnterResponse;
|
||||||
|
|
||||||
/// Which directions a solid may be pushed in.
|
/// Which directions a solid may be pushed in.
|
||||||
///
|
///
|
||||||
/// Only meaningful for `solid` cells (a non-solid cell is passable, so nothing is
|
/// Only meaningful for `solid` cells (a non-solid cell is passable, so nothing is
|
||||||
/// ever pushed into it). `Crate` is [`Pushable::Any`]; the directional crates
|
/// ever pushed into it). `Crate` is [`Pushable::Any`]; the directional crates
|
||||||
/// constrain pushes to one axis.
|
/// constrain pushes to one axis.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum Pushable {
|
pub enum Pushable {
|
||||||
/// Cannot be pushed.
|
/// Cannot be pushed.
|
||||||
No,
|
No,
|
||||||
@@ -61,6 +64,9 @@ pub struct Behavior {
|
|||||||
/// the thing is expected to remove itself via `die()`). Only meaningful when
|
/// the thing is expected to remove itself via `die()`). Only meaningful when
|
||||||
/// `solid`. The same grab fires if the thing is pushed into the player.
|
/// `solid`. The same grab fires if the thing is pushed into the player.
|
||||||
pub grab: bool,
|
pub grab: bool,
|
||||||
|
/// The radius of light this emits, if any. Light _color_ is determined by the
|
||||||
|
/// glyph foreground color of whatever it is.
|
||||||
|
pub glow: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A stable, unique identifier for a board object.
|
/// A stable, unique identifier for a board object.
|
||||||
@@ -133,6 +139,7 @@ impl Solid {
|
|||||||
opaque: true,
|
opaque: true,
|
||||||
pushable: Pushable::Any,
|
pushable: Pushable::Any,
|
||||||
grab: false,
|
grab: false,
|
||||||
|
glow: 6 // TODO player should not inherently glow, but glowing inventory items need to wait for inventory
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,18 +192,22 @@ impl Solid {
|
|||||||
|
|
||||||
/// Which directions the occupant may be pushed (the player → [`Pushable::Any`]).
|
/// Which directions the occupant may be pushed (the player → [`Pushable::Any`]).
|
||||||
pub fn pushable(&self) -> Pushable {
|
pub fn pushable(&self) -> Pushable {
|
||||||
self.behavior.pushable
|
if let EnterResponse::Push(p) = EnterResponse::from(self.behavior) {
|
||||||
|
p
|
||||||
|
} else {
|
||||||
|
Pushable::No
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this occupant is a **grab** thing (a gem-like solid the player
|
/// Whether this occupant is a **grab** thing (a gem-like solid the player
|
||||||
/// collects on contact). The player is never grabbable.
|
/// collects on contact). The player is never grabbable.
|
||||||
pub fn grab(&self) -> bool {
|
pub fn grab(&self) -> bool {
|
||||||
self.behavior.grab
|
EnterResponse::from(self.behavior) == EnterResponse::Grab
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this occupant blocks movement (always true for a `Solid`).
|
/// Whether this occupant blocks movement (always true for a `Solid`).
|
||||||
pub fn solid(&self) -> bool {
|
pub fn solid(&self) -> bool {
|
||||||
self.behavior.solid
|
EnterResponse::from(self.behavior) == EnterResponse::Block
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this occupant blocks line of sight.
|
/// Whether this occupant blocks line of sight.
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ mod tests {
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use crate::Builtin;
|
||||||
|
|
||||||
/// A `deep_clone`d world owns independent boards: mutating the copy must leave the
|
/// A `deep_clone`d world owns independent boards: mutating the copy must leave the
|
||||||
/// original board untouched (the property the editor's playtest relies on).
|
/// original board untouched (the property the editor's playtest relies on).
|
||||||
@@ -155,14 +156,14 @@ mod tests {
|
|||||||
copy.boards["start"].borrow_mut().place_archetype(
|
copy.boards["start"].borrow_mut().place_archetype(
|
||||||
1,
|
1,
|
||||||
0,
|
0,
|
||||||
Archetype::Wall,
|
Archetype::Builtin(Builtin::Wall, "wall"),
|
||||||
Archetype::Wall.default_glyph(),
|
Builtin::Wall.default_glyph_for("wall"),
|
||||||
);
|
);
|
||||||
|
|
||||||
// The copy changed; the original is still empty at that cell.
|
// The copy changed; the original is still empty at that cell.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
copy.boards["start"].borrow().get(1, 0).1,
|
copy.boards["start"].borrow().get(1, 0).1,
|
||||||
Archetype::Wall
|
Archetype::Builtin(Builtin::Wall, "wall")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
world.boards["start"].borrow().get(1, 0).1,
|
world.boards["start"].borrow().get(1, 0).1,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use kiln_core::game::GameState;
|
|||||||
use kiln_core::glyph::Glyph;
|
use kiln_core::glyph::Glyph;
|
||||||
use kiln_core::log::LogLine;
|
use kiln_core::log::LogLine;
|
||||||
use kiln_core::world::World;
|
use kiln_core::world::World;
|
||||||
use kiln_core::{Archetype, Board};
|
use kiln_core::{Archetype, Board, Builtin};
|
||||||
use kiln_ui::code_editor::{CodeEditor, CodeEditorOutcome};
|
use kiln_ui::code_editor::{CodeEditor, CodeEditorOutcome};
|
||||||
use kiln_ui::dialog::{Dialog, DialogResult, ListDialogResponse};
|
use kiln_ui::dialog::{Dialog, DialogResult, ListDialogResponse};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
@@ -112,8 +112,8 @@ impl EditorState {
|
|||||||
code_editor: None,
|
code_editor: None,
|
||||||
glyph_dialog: None,
|
glyph_dialog: None,
|
||||||
cursor,
|
cursor,
|
||||||
current_archetype: Archetype::Wall,
|
current_archetype: Archetype::Builtin(Builtin::Wall, "wall"),
|
||||||
current_glyph: Archetype::Wall.default_glyph(),
|
current_glyph: Builtin::Wall.default_glyph_for("wall"),
|
||||||
draw_mode: false,
|
draw_mode: false,
|
||||||
sidebar_width: DEFAULT_SIDEBAR_WIDTH,
|
sidebar_width: DEFAULT_SIDEBAR_WIDTH,
|
||||||
blink_on: true,
|
blink_on: true,
|
||||||
|
|||||||
@@ -77,10 +77,10 @@ impl MenuLevel {
|
|||||||
MenuEntry::item('c', "Custom glyph [TODO]", |_| {}),
|
MenuEntry::item('c', "Custom glyph [TODO]", |_| {}),
|
||||||
],
|
],
|
||||||
MenuLevel::Terrain => vec![
|
MenuLevel::Terrain => vec![
|
||||||
MenuEntry::item('w', "Wall", |ed| ed.set_current(Archetype::Wall)),
|
MenuEntry::item('w', "Wall", |ed| ed.set_current(Archetype::Builtin(Builtin::Wall, "wall"))),
|
||||||
MenuEntry::item('c', "■ Crate", |ed| ed.set_current(Archetype::Crate)),
|
MenuEntry::item('c', "■ Crate", |ed| ed.set_current(Archetype::Builtin(Builtin::Crate, "crate"))),
|
||||||
MenuEntry::item('v', "↕ Crate", |ed| ed.set_current(Archetype::VCrate)),
|
MenuEntry::item('v', "↕ Crate", |ed| ed.set_current(Archetype::Builtin(Builtin::VCrate, "vcrate"))),
|
||||||
MenuEntry::item('h', "↔ Crate", |ed| ed.set_current(Archetype::HCrate)),
|
MenuEntry::item('h', "↔ Crate", |ed| ed.set_current(Archetype::Builtin(Builtin::HCrate, "hcrate"))),
|
||||||
],
|
],
|
||||||
MenuLevel::Machines => vec![
|
MenuLevel::Machines => vec![
|
||||||
MenuEntry::item('p', "Pushers...", |ed| ed.menu.push(MenuLevel::Pushers)),
|
MenuEntry::item('p', "Pushers...", |ed| ed.menu.push(MenuLevel::Pushers)),
|
||||||
|
|||||||
Reference in New Issue
Block a user