some auto-refactoring
This commit is contained in:
@@ -41,8 +41,8 @@ pub(crate) enum Action {
|
|||||||
Log(LogLine),
|
Log(LogLine),
|
||||||
/// Add (`present = true`) or remove (`present = false`) `tag` on `target`.
|
/// Add (`present = true`) or remove (`present = false`) `tag` on `target`.
|
||||||
SetTag { target: ObjectId, tag: String, present: bool },
|
SetTag { target: ObjectId, tag: String, present: bool },
|
||||||
/// Display a speech bubble above the source object for [`crate::game::SAY_DURATION`] seconds.
|
/// Display a speech bubble above the source object for `duration` seconds.
|
||||||
Say(String),
|
Say(String, f64),
|
||||||
/// Pause draining this object's queue for `secs` seconds. Never reaches the board queue.
|
/// Pause draining this object's queue for `secs` seconds. Never reaches the board queue.
|
||||||
Delay(f64),
|
Delay(f64),
|
||||||
/// Set the source object's foreground and/or background color.
|
/// Set the source object's foreground and/or background color.
|
||||||
@@ -95,9 +95,10 @@ pub(crate) fn action_to_map(action: &Action) -> rhai::Map {
|
|||||||
m.insert("tag".into(), Dynamic::from(tag.clone()));
|
m.insert("tag".into(), Dynamic::from(tag.clone()));
|
||||||
m.insert("present".into(), Dynamic::from(*present));
|
m.insert("present".into(), Dynamic::from(*present));
|
||||||
}
|
}
|
||||||
Action::Say(text) => {
|
Action::Say(text, dur) => {
|
||||||
m.insert("type".into(), ds("Say"));
|
m.insert("type".into(), ds("Say"));
|
||||||
m.insert("msg".into(), Dynamic::from(text.clone()));
|
m.insert("msg".into(), Dynamic::from(text.clone()));
|
||||||
|
m.insert("duration".into(), Dynamic::from(*dur));
|
||||||
}
|
}
|
||||||
Action::Delay(secs) => {
|
Action::Delay(secs) => {
|
||||||
m.insert("type".into(), ds("Delay"));
|
m.insert("type".into(), ds("Delay"));
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use crate::archetype::Archetype;
|
use crate::archetype::Archetype;
|
||||||
use crate::archetype::Archetype::Empty;
|
use crate::archetype::Archetype::Empty;
|
||||||
use crate::font::FontSpec;
|
|
||||||
use crate::glyph::Glyph;
|
use crate::glyph::Glyph;
|
||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
use crate::object_def::ObjectDef;
|
use crate::object_def::ObjectDef;
|
||||||
@@ -57,10 +56,6 @@ pub struct Board {
|
|||||||
pub next_object_id: ObjectId,
|
pub next_object_id: ObjectId,
|
||||||
/// Portals on this board. Parsed from the map file; not yet active.
|
/// Portals on this board. Parsed from the map file; not yet active.
|
||||||
pub portals: Vec<PortalDef>,
|
pub portals: Vec<PortalDef>,
|
||||||
/// Optional font override for this board. When `None`, the app default is used.
|
|
||||||
pub font: Option<FontSpec>,
|
|
||||||
/// Integer scale factor applied to tiles when rendering this board. 1 = natural size.
|
|
||||||
pub zoom: u32,
|
|
||||||
/// Name of the board-level script in the world script pool, if any.
|
/// Name of the board-level script in the world script pool, if any.
|
||||||
///
|
///
|
||||||
/// A board script runs on the board as a whole (e.g. `on_enter`, `on_tick`)
|
/// A board script runs on the board as a whole (e.g. `on_enter`, `on_tick`)
|
||||||
@@ -343,8 +338,6 @@ pub(crate) mod tests {
|
|||||||
objects: object_map,
|
objects: object_map,
|
||||||
next_object_id,
|
next_object_id,
|
||||||
portals: Vec::new(),
|
portals: Vec::new(),
|
||||||
font: None,
|
|
||||||
zoom: 1,
|
|
||||||
board_script_name: None,
|
board_script_name: None,
|
||||||
load_errors: Vec::new(),
|
load_errors: Vec::new(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
/// Specifies a bitmap font for a board.
|
|
||||||
///
|
|
||||||
/// Each board can optionally specify a font image and tile dimensions.
|
|
||||||
/// When absent, the app's default embedded CP437 font is used.
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
|
||||||
pub struct FontSpec {
|
|
||||||
/// Path to the PNG font image, relative to the working directory.
|
|
||||||
pub path: String,
|
|
||||||
/// Width of each tile in the font image, in pixels.
|
|
||||||
pub tile_w: u32,
|
|
||||||
/// Height of each tile in the font image, in pixels.
|
|
||||||
pub tile_h: u32,
|
|
||||||
}
|
|
||||||
@@ -197,10 +197,10 @@ impl GameState {
|
|||||||
}
|
}
|
||||||
// Replace any existing bubble from this object so repeated say() calls
|
// Replace any existing bubble from this object so repeated say() calls
|
||||||
// don't stack visually — the new text resets the timer.
|
// don't stack visually — the new text resets the timer.
|
||||||
Action::Say(text) => new_bubbles.push(SpeechBubble {
|
Action::Say(text, duration) => new_bubbles.push(SpeechBubble {
|
||||||
object_id: ba.source,
|
object_id: ba.source,
|
||||||
text,
|
text,
|
||||||
remaining: SAY_DURATION,
|
remaining: duration,
|
||||||
}),
|
}),
|
||||||
// Delays are consumed by ScriptHost::drain and never reach the board queue.
|
// Delays are consumed by ScriptHost::drain and never reach the board queue.
|
||||||
Action::Delay(_) => {}
|
Action::Delay(_) => {}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ pub mod world;
|
|||||||
pub mod script;
|
pub mod script;
|
||||||
pub mod glyph;
|
pub mod glyph;
|
||||||
mod action;
|
mod action;
|
||||||
mod font;
|
|
||||||
mod utils;
|
mod utils;
|
||||||
mod archetype;
|
mod archetype;
|
||||||
mod object_def;
|
mod object_def;
|
||||||
|
|||||||
+71
-116
@@ -3,11 +3,11 @@ use crate::board::Board;
|
|||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
use color::Rgba8;
|
use color::Rgba8;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use crate::archetype::Archetype;
|
use crate::archetype::Archetype;
|
||||||
use crate::font::FontSpec;
|
|
||||||
use crate::glyph::Glyph;
|
use crate::glyph::Glyph;
|
||||||
use crate::object_def::ObjectDef;
|
use crate::object_def::ObjectDef;
|
||||||
use crate::utils::{ObjectId, Player, PortalDef};
|
use crate::utils::{ObjectId, Player, PortalDef};
|
||||||
@@ -28,9 +28,6 @@ pub struct MapFile {
|
|||||||
pub palette: HashMap<String, PaletteEntry>,
|
pub palette: HashMap<String, PaletteEntry>,
|
||||||
/// The `[grid]` section: the multi-line string that defines cell layout.
|
/// The `[grid]` section: the multi-line string that defines cell layout.
|
||||||
pub grid: GridData,
|
pub grid: GridData,
|
||||||
/// Optional `[font]` section specifying a bitmap font for this board.
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub font: Option<FontHeader>,
|
|
||||||
/// Optional `[floor]` declaration: the visual floor layer (a generator name,
|
/// Optional `[floor]` declaration: the visual floor layer (a generator name,
|
||||||
/// a single glyph, or a grid with its own palette). See [`FloorSpec`].
|
/// a single glyph, or a grid with its own palette). See [`FloorSpec`].
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
@@ -58,22 +55,6 @@ pub struct MapHeader {
|
|||||||
/// Name of the board-level script in the `[scripts]` table, if any.
|
/// Name of the board-level script in the `[scripts]` table, if any.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub board_script_name: Option<String>,
|
pub board_script_name: Option<String>,
|
||||||
/// Integer tile scale factor. 1 = natural size. Omitted from files when 1 (the default).
|
|
||||||
#[serde(default = "default_zoom", skip_serializing_if = "is_zoom_default")]
|
|
||||||
pub zoom: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The `[font]` section of a map file, specifying a bitmap font for this board.
|
|
||||||
///
|
|
||||||
/// When absent, the app's default embedded CP437 font is used.
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
pub struct FontHeader {
|
|
||||||
/// Path to the PNG font image, relative to the working directory.
|
|
||||||
pub path: String,
|
|
||||||
/// Width of each tile in the font image, in pixels.
|
|
||||||
pub tile_w: u32,
|
|
||||||
/// Height of each tile in the font image, in pixels.
|
|
||||||
pub tile_h: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A tile index in a palette entry: either a plain integer or a character literal.
|
/// A tile index in a palette entry: either a plain integer or a character literal.
|
||||||
@@ -200,11 +181,59 @@ pub(crate) struct MapFileObjectEntry {
|
|||||||
fn default_true() -> bool {
|
fn default_true() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
fn default_zoom() -> u32 {
|
|
||||||
1
|
/// Constructs a [`Glyph`] from the three raw TOML-sourced fields.
|
||||||
|
fn make_glyph(tile: u32, fg: &str, bg: &str) -> Glyph {
|
||||||
|
Glyph { tile, fg: parse_color(fg), bg: parse_color(bg) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a serializable [`PaletteEntry`] from core board types.
|
||||||
|
fn make_palette_entry(glyph: Glyph, arch: Archetype) -> PaletteEntry {
|
||||||
|
PaletteEntry {
|
||||||
|
archetype: arch.name().to_string(),
|
||||||
|
tile: TileIndex::Num(glyph.tile),
|
||||||
|
fg: color_to_hex(glyph.fg),
|
||||||
|
bg: color_to_hex(glyph.bg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the player's starting cell from `player_start`, falling back to `(0, 0)` on any error.
|
||||||
|
fn resolve_player_start(
|
||||||
|
start: &PlayerStart,
|
||||||
|
w: usize,
|
||||||
|
h: usize,
|
||||||
|
player_grid_pos: Option<(usize, usize)>,
|
||||||
|
player_grid_count: usize,
|
||||||
|
errors: &mut Vec<LogLine>,
|
||||||
|
) -> (usize, usize) {
|
||||||
|
match start {
|
||||||
|
PlayerStart::Coord([x, y])
|
||||||
|
if *x >= 0 && *y >= 0 && (*x as usize) < w && (*y as usize) < h =>
|
||||||
|
{
|
||||||
|
(*x as usize, *y as usize)
|
||||||
|
}
|
||||||
|
PlayerStart::Coord([x, y]) => {
|
||||||
|
errors.push(LogLine::error(format!(
|
||||||
|
"player_start ({x}, {y}) is out of bounds; placing player at (0, 0)"
|
||||||
|
)));
|
||||||
|
(0, 0)
|
||||||
|
}
|
||||||
|
PlayerStart::Char(c) => match (player_grid_pos, player_grid_count) {
|
||||||
|
(Some(pos), 1) => pos,
|
||||||
|
(Some(pos), n) => {
|
||||||
|
errors.push(LogLine::error(format!(
|
||||||
|
"player char '{c}' appears {n} times in the grid; using the first"
|
||||||
|
)));
|
||||||
|
pos
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
errors.push(LogLine::error(format!(
|
||||||
|
"player char '{c}' not found in the grid; placing player at (0, 0)"
|
||||||
|
)));
|
||||||
|
(0, 0)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
fn is_zoom_default(z: &u32) -> bool {
|
|
||||||
*z == 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses an `"#RRGGBB"` hex color string into an [`Rgba8`].
|
/// Parses an `"#RRGGBB"` hex color string into an [`Rgba8`].
|
||||||
@@ -270,11 +299,7 @@ impl TryFrom<MapFile> for Board {
|
|||||||
let tile = entry.tile.into_u32();
|
let tile = entry.tile.into_u32();
|
||||||
match Archetype::try_from(entry.archetype.as_str()) {
|
match Archetype::try_from(entry.archetype.as_str()) {
|
||||||
Ok(archetype) => {
|
Ok(archetype) => {
|
||||||
let glyph = Glyph {
|
let glyph = make_glyph(tile, &entry.fg, &entry.bg);
|
||||||
tile,
|
|
||||||
fg: parse_color(&entry.fg),
|
|
||||||
bg: parse_color(&entry.bg),
|
|
||||||
};
|
|
||||||
palette.insert(key_char, (glyph, archetype));
|
palette.insert(key_char, (glyph, archetype));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -351,7 +376,6 @@ impl TryFrom<MapFile> for Board {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// It must be unique across objects — keep the first claimant, drop later ones.
|
// It must be unique across objects — keep the first claimant, drop later ones.
|
||||||
use std::collections::hash_map::Entry;
|
|
||||||
match palette_char_owner.entry(ch) {
|
match palette_char_owner.entry(ch) {
|
||||||
Entry::Occupied(_) => {
|
Entry::Occupied(_) => {
|
||||||
load_errors.push(LogLine::error(format!(
|
load_errors.push(LogLine::error(format!(
|
||||||
@@ -402,7 +426,7 @@ impl TryFrom<MapFile> for Board {
|
|||||||
if skip[owner] {
|
if skip[owner] {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if obj_palette_positions.get(&ch).map_or(true, Vec::is_empty) {
|
if obj_palette_positions.get(&ch).is_none_or(Vec::is_empty) {
|
||||||
load_errors.push(LogLine::error(format!(
|
load_errors.push(LogLine::error(format!(
|
||||||
"object palette char '{ch}' not found in the grid; skipping object"
|
"object palette char '{ch}' not found in the grid; skipping object"
|
||||||
)));
|
)));
|
||||||
@@ -410,12 +434,6 @@ impl TryFrom<MapFile> for Board {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let font = mf.font.map(|f| FontSpec {
|
|
||||||
path: f.path,
|
|
||||||
tile_w: f.tile_w,
|
|
||||||
tile_h: f.tile_h,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build the cached floor layer from the (optional) `[floor]` declaration.
|
// Build the cached floor layer from the (optional) `[floor]` declaration.
|
||||||
// Best-effort: any problem is recorded on `load_errors`, not fatal. The
|
// Best-effort: any problem is recorded on `load_errors`, not fatal. The
|
||||||
// raw spec is kept on the board so a save can round-trip it.
|
// raw spec is kept on the board so a save can round-trip it.
|
||||||
@@ -430,34 +448,14 @@ impl TryFrom<MapFile> for Board {
|
|||||||
// Resolve the player's cell (nonfatal; fall back to (0, 0) on any problem),
|
// Resolve the player's cell (nonfatal; fall back to (0, 0) on any problem),
|
||||||
// then let the player *win* its cell: clear solid terrain under it and claim
|
// then let the player *win* its cell: clear solid terrain under it and claim
|
||||||
// the cell so a solid object placed here is dropped by the loop below.
|
// the cell so a solid object placed here is dropped by the loop below.
|
||||||
let (px, py) = match mf.map.player_start {
|
let (px, py) = resolve_player_start(
|
||||||
PlayerStart::Coord([x, y])
|
&mf.map.player_start,
|
||||||
if x >= 0 && y >= 0 && (x as usize) < w && (y as usize) < h =>
|
w,
|
||||||
{
|
h,
|
||||||
(x as usize, y as usize)
|
player_grid_pos,
|
||||||
}
|
player_grid_count,
|
||||||
PlayerStart::Coord([x, y]) => {
|
&mut load_errors,
|
||||||
load_errors.push(LogLine::error(format!(
|
);
|
||||||
"player_start ({x}, {y}) is out of bounds; placing player at (0, 0)"
|
|
||||||
)));
|
|
||||||
(0, 0)
|
|
||||||
}
|
|
||||||
PlayerStart::Char(c) => match (player_grid_pos, player_grid_count) {
|
|
||||||
(Some(pos), 1) => pos,
|
|
||||||
(Some(pos), n) => {
|
|
||||||
load_errors.push(LogLine::error(format!(
|
|
||||||
"player char '{c}' appears {n} times in the grid; using the first"
|
|
||||||
)));
|
|
||||||
pos
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
load_errors.push(LogLine::error(format!(
|
|
||||||
"player char '{c}' not found in the grid; placing player at (0, 0)"
|
|
||||||
)));
|
|
||||||
(0, 0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let pidx = py * w + px;
|
let pidx = py * w + px;
|
||||||
if solid_occupied[pidx] {
|
if solid_occupied[pidx] {
|
||||||
cells[pidx] = canonical_empty;
|
cells[pidx] = canonical_empty;
|
||||||
@@ -504,7 +502,6 @@ impl TryFrom<MapFile> for Board {
|
|||||||
// Validate name uniqueness: first claimant keeps the name, later ones
|
// Validate name uniqueness: first claimant keeps the name, later ones
|
||||||
// have their name cleared and a nonfatal error is recorded.
|
// have their name cleared and a nonfatal error is recorded.
|
||||||
e.name.as_ref().and_then(|n| {
|
e.name.as_ref().and_then(|n| {
|
||||||
use std::collections::hash_map::Entry;
|
|
||||||
match seen_names.entry(n.clone()) {
|
match seen_names.entry(n.clone()) {
|
||||||
Entry::Vacant(v) => { v.insert(i); Some(n.clone()) }
|
Entry::Vacant(v) => { v.insert(i); Some(n.clone()) }
|
||||||
Entry::Occupied(o) => {
|
Entry::Occupied(o) => {
|
||||||
@@ -524,11 +521,7 @@ impl TryFrom<MapFile> for Board {
|
|||||||
id: 0, // stamped by Board::add_object
|
id: 0, // stamped by Board::add_object
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
glyph: Glyph {
|
glyph: make_glyph(e.tile.into_u32(), &e.fg, &e.bg),
|
||||||
tile: e.tile.into_u32(),
|
|
||||||
fg: parse_color(&e.fg),
|
|
||||||
bg: parse_color(&e.bg),
|
|
||||||
},
|
|
||||||
solid: e.solid,
|
solid: e.solid,
|
||||||
opaque: e.opaque,
|
opaque: e.opaque,
|
||||||
pushable: e.pushable,
|
pushable: e.pushable,
|
||||||
@@ -572,8 +565,6 @@ impl TryFrom<MapFile> for Board {
|
|||||||
objects,
|
objects,
|
||||||
next_object_id,
|
next_object_id,
|
||||||
portals: mf.portals,
|
portals: mf.portals,
|
||||||
font,
|
|
||||||
zoom: mf.map.zoom,
|
|
||||||
board_script_name: mf.map.board_script_name,
|
board_script_name: mf.map.board_script_name,
|
||||||
load_errors,
|
load_errors,
|
||||||
})
|
})
|
||||||
@@ -607,33 +598,14 @@ impl From<&Board> for MapFile {
|
|||||||
for x in 0..board.width {
|
for x in 0..board.width {
|
||||||
let (glyph, arch) = board.get(x, y);
|
let (glyph, arch) = board.get(x, y);
|
||||||
let key = (*glyph, *arch);
|
let key = (*glyph, *arch);
|
||||||
if let std::collections::hash_map::Entry::Vacant(e) = cell_to_key.entry(key) {
|
if let Entry::Vacant(e) = cell_to_key.entry(key) {
|
||||||
if key == canonical_empty {
|
let ch = if key == canonical_empty {
|
||||||
e.insert(' ');
|
' '
|
||||||
palette.insert(
|
|
||||||
" ".to_string(),
|
|
||||||
PaletteEntry {
|
|
||||||
archetype: arch.name().to_string(),
|
|
||||||
tile: TileIndex::Num(glyph.tile),
|
|
||||||
fg: color_to_hex(glyph.fg),
|
|
||||||
bg: color_to_hex(glyph.bg),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
let ch = *pool_iter
|
*pool_iter.next().expect("board has more than 92 unique non-canonical cell types")
|
||||||
.next()
|
};
|
||||||
.expect("board has more than 92 unique non-canonical cell types");
|
|
||||||
e.insert(ch);
|
e.insert(ch);
|
||||||
palette.insert(
|
palette.insert(ch.to_string(), make_palette_entry(*glyph, *arch));
|
||||||
ch.to_string(),
|
|
||||||
PaletteEntry {
|
|
||||||
archetype: arch.name().to_string(),
|
|
||||||
tile: TileIndex::Num(glyph.tile),
|
|
||||||
fg: color_to_hex(glyph.fg),
|
|
||||||
bg: color_to_hex(glyph.bg),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -651,12 +623,6 @@ impl From<&Board> for MapFile {
|
|||||||
// Trailing newline ensures the last row is complete; str::lines handles it correctly.
|
// Trailing newline ensures the last row is complete; str::lines handles it correctly.
|
||||||
let content = rows.join("\n") + "\n";
|
let content = rows.join("\n") + "\n";
|
||||||
|
|
||||||
let font = board.font.as_ref().map(|f| FontHeader {
|
|
||||||
path: f.path.clone(),
|
|
||||||
tile_w: f.tile_w,
|
|
||||||
tile_h: f.tile_h,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Convert ObjectDef → MapFileObjectEntry, encoding the glyph as TOML-compatible fields.
|
// Convert ObjectDef → MapFileObjectEntry, encoding the glyph as TOML-compatible fields.
|
||||||
let objects: Vec<MapFileObjectEntry> = board
|
let objects: Vec<MapFileObjectEntry> = board
|
||||||
.objects
|
.objects
|
||||||
@@ -680,16 +646,7 @@ impl From<&Board> for MapFile {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let portals = board
|
let portals = board.portals.clone();
|
||||||
.portals
|
|
||||||
.iter()
|
|
||||||
.map(|p| PortalDef {
|
|
||||||
x: p.x,
|
|
||||||
y: p.y,
|
|
||||||
target_map: p.target_map.clone(),
|
|
||||||
target_entry: p.target_entry.clone(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
MapFile {
|
MapFile {
|
||||||
map: MapHeader {
|
map: MapHeader {
|
||||||
@@ -698,11 +655,9 @@ impl From<&Board> for MapFile {
|
|||||||
height: board.height,
|
height: board.height,
|
||||||
player_start: PlayerStart::Coord([board.player.x, board.player.y]),
|
player_start: PlayerStart::Coord([board.player.x, board.player.y]),
|
||||||
board_script_name: board.board_script_name.clone(),
|
board_script_name: board.board_script_name.clone(),
|
||||||
zoom: board.zoom,
|
|
||||||
},
|
},
|
||||||
palette,
|
palette,
|
||||||
grid: GridData { content },
|
grid: GridData { content },
|
||||||
font,
|
|
||||||
// Round-trip the original floor declaration verbatim (generators
|
// Round-trip the original floor declaration verbatim (generators
|
||||||
// re-randomize on the next load; literal glyph grids are exact).
|
// re-randomize on the next load; literal glyph grids are exact).
|
||||||
floor: board.floor_spec.clone(),
|
floor: board.floor_spec.clone(),
|
||||||
|
|||||||
+49
-28
@@ -9,15 +9,22 @@
|
|||||||
//! - `bump(id)` — run when another mover steps into this object's cell, with the
|
//! - `bump(id)` — run when another mover steps into this object's cell, with the
|
||||||
//! bumper's object index (`-1` for the player); see [`ScriptHost::run_bump`].
|
//! bumper's object index (`-1` for the player); see [`ScriptHost::run_bump`].
|
||||||
//!
|
//!
|
||||||
//! ## Script constants in scope
|
//! ## Script constants
|
||||||
//!
|
//!
|
||||||
//! Every object scope contains:
|
//! Object-specific handles live in the per-object scope (visible to the
|
||||||
|
//! top-level hook Rust calls):
|
||||||
//!
|
//!
|
||||||
//! | Name | Type | Description |
|
//! | Name | Type | Description |
|
||||||
//! |---|---|---|
|
//! |---|---|---|
|
||||||
//! | `Board` | `Board` | Read-only world handle. |
|
//! | `Board` | `Board` | Read-only world handle. |
|
||||||
//! | `Queue` | `Queue` | This object's pending-action queue. |
|
//! | `Queue` | `Queue` | This object's pending-action queue. |
|
||||||
//! | `Me` | `Me` | Self-reference (id, name, x, y, tags, glyph). |
|
//! | `Me` | `Me` | Self-reference (id, name, x, y, tags, glyph). |
|
||||||
|
//!
|
||||||
|
//! Direction and color constants are registered as a global Rhai module and
|
||||||
|
//! are visible to all functions at any call depth, including Rhai-to-Rhai calls:
|
||||||
|
//!
|
||||||
|
//! | Name | Type | Description |
|
||||||
|
//! |---|---|---|
|
||||||
//! | `North`/`South`/`East`/`West` | `Direction` | Cardinal directions. |
|
//! | `North`/`South`/`East`/`West` | `Direction` | Cardinal directions. |
|
||||||
//! | `Black`..`White` | `String` | 16 EGA/VGA color hex strings. |
|
//! | `Black`..`White` | `String` | 16 EGA/VGA color hex strings. |
|
||||||
//!
|
//!
|
||||||
@@ -46,11 +53,12 @@
|
|||||||
//! `Queue.length()`, `Queue.clear()`, `Queue.peek()`, `Queue.pop()`
|
//! `Queue.length()`, `Queue.clear()`, `Queue.peek()`, `Queue.pop()`
|
||||||
|
|
||||||
use crate::action::{action_to_map, Action, ScrollLine, MOVE_COST};
|
use crate::action::{action_to_map, Action, ScrollLine, MOVE_COST};
|
||||||
|
use crate::game::SAY_DURATION;
|
||||||
use crate::board::Board;
|
use crate::board::Board;
|
||||||
use crate::log::LogLine;
|
use crate::log::LogLine;
|
||||||
use crate::map_file::{color_to_hex, parse_color};
|
use crate::map_file::{color_to_hex, parse_color};
|
||||||
use crate::utils::{Direction, ObjectId, ScriptArg};
|
use crate::utils::{Direction, ObjectId, ScriptArg};
|
||||||
use rhai::{Array, CallFnOptions, Dynamic, Engine, FuncArgs, ImmutableString, NativeCallContext, Scope, AST};
|
use rhai::{Array, CallFnOptions, Dynamic, Engine, FuncArgs, ImmutableString, Module, NativeCallContext, Scope, AST};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
@@ -179,6 +187,7 @@ impl ScriptHost {
|
|||||||
register_me_type(&mut engine);
|
register_me_type(&mut engine);
|
||||||
register_object_info_type(&mut engine);
|
register_object_info_type(&mut engine);
|
||||||
register_glyph_type(&mut engine);
|
register_glyph_type(&mut engine);
|
||||||
|
register_global_constants(&mut engine);
|
||||||
|
|
||||||
let board = board_cell.borrow();
|
let board = board_cell.borrow();
|
||||||
|
|
||||||
@@ -612,7 +621,11 @@ fn register_write_api(engine: &mut Engine, queues: &QueueMap) {
|
|||||||
|
|
||||||
let q = queues.clone();
|
let q = queues.clone();
|
||||||
engine.register_fn("say", move |ctx: NativeCallContext, msg: ImmutableString| {
|
engine.register_fn("say", move |ctx: NativeCallContext, msg: ImmutableString| {
|
||||||
emit(&q, source_of(&ctx), Action::Say(msg.to_string()));
|
emit(&q, source_of(&ctx), Action::Say(msg.to_string(), SAY_DURATION));
|
||||||
|
});
|
||||||
|
let q = queues.clone();
|
||||||
|
engine.register_fn("say", move |ctx: NativeCallContext, msg: ImmutableString, dur: f64| {
|
||||||
|
emit(&q, source_of(&ctx), Action::Say(msg.to_string(), dur));
|
||||||
});
|
});
|
||||||
|
|
||||||
// scroll(lines): open a full-screen scrollable overlay. Each element of `lines`
|
// scroll(lines): open a full-screen scrollable overlay. Each element of `lines`
|
||||||
@@ -739,36 +752,44 @@ fn register_queue_api(engine: &mut Engine) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Scope construction ────────────────────────────────────────────────────────
|
// ── Scope construction & global constants ─────────────────────────────────────
|
||||||
|
|
||||||
/// Builds a fresh per-object scope with the Board view, Queue, Me, direction
|
/// Registers direction and color constants as a global Rhai module so they are
|
||||||
/// constants, and the 16 EGA/VGA named color constants.
|
/// visible to every function at any call depth, including Rhai-to-Rhai calls.
|
||||||
|
/// (Scope-level constants are only visible to the top-level function Rust calls.)
|
||||||
|
fn register_global_constants(engine: &mut Engine) {
|
||||||
|
let mut m = Module::new();
|
||||||
|
m.set_var("North", Direction::North);
|
||||||
|
m.set_var("South", Direction::South);
|
||||||
|
m.set_var("East", Direction::East);
|
||||||
|
m.set_var("West", Direction::West);
|
||||||
|
// 16 EGA/VGA color constants as "#RRGGBB" strings.
|
||||||
|
m.set_var("Black", "#000000");
|
||||||
|
m.set_var("Blue", "#0000AA");
|
||||||
|
m.set_var("Green", "#00AA00");
|
||||||
|
m.set_var("Cyan", "#00AAAA");
|
||||||
|
m.set_var("Red", "#AA0000");
|
||||||
|
m.set_var("Magenta", "#AA00AA");
|
||||||
|
m.set_var("Brown", "#AA5500");
|
||||||
|
m.set_var("LightGray", "#AAAAAA");
|
||||||
|
m.set_var("DarkGray", "#555555");
|
||||||
|
m.set_var("BrightBlue", "#5555FF");
|
||||||
|
m.set_var("BrightGreen", "#55FF55");
|
||||||
|
m.set_var("BrightCyan", "#55FFFF");
|
||||||
|
m.set_var("BrightRed", "#FF5555");
|
||||||
|
m.set_var("BrightMagenta", "#FF55FF");
|
||||||
|
m.set_var("Yellow", "#FFFF55");
|
||||||
|
m.set_var("White", "#FFFFFF");
|
||||||
|
engine.register_global_module(m.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a fresh per-object scope containing only the object-specific handles:
|
||||||
|
/// the Board view, the object's Queue, and the Me self-reference.
|
||||||
fn new_object_scope(board: &BoardRef, queue: &ObjQueue, object_id: ObjectId) -> Scope<'static> {
|
fn new_object_scope(board: &BoardRef, queue: &ObjQueue, object_id: ObjectId) -> Scope<'static> {
|
||||||
let mut scope = Scope::new();
|
let mut scope = Scope::new();
|
||||||
scope.push_constant("Board", board.clone());
|
scope.push_constant("Board", board.clone());
|
||||||
scope.push_constant("Queue", queue.clone());
|
scope.push_constant("Queue", queue.clone());
|
||||||
scope.push_constant("Me", Me { object_id, board: board.clone() });
|
scope.push_constant("Me", Me { object_id, board: board.clone() });
|
||||||
scope.push_constant("North", Direction::North);
|
|
||||||
scope.push_constant("South", Direction::South);
|
|
||||||
scope.push_constant("East", Direction::East);
|
|
||||||
scope.push_constant("West", Direction::West);
|
|
||||||
// 16 EGA/VGA color constants as "#RRGGBB" strings.
|
|
||||||
scope.push_constant("Black", "#000000");
|
|
||||||
scope.push_constant("Blue", "#0000AA");
|
|
||||||
scope.push_constant("Green", "#00AA00");
|
|
||||||
scope.push_constant("Cyan", "#00AAAA");
|
|
||||||
scope.push_constant("Red", "#AA0000");
|
|
||||||
scope.push_constant("Magenta", "#AA00AA");
|
|
||||||
scope.push_constant("Brown", "#AA5500");
|
|
||||||
scope.push_constant("LightGray", "#AAAAAA");
|
|
||||||
scope.push_constant("DarkGray", "#555555");
|
|
||||||
scope.push_constant("BrightBlue", "#5555FF");
|
|
||||||
scope.push_constant("BrightGreen", "#55FF55");
|
|
||||||
scope.push_constant("BrightCyan", "#55FFFF");
|
|
||||||
scope.push_constant("BrightRed", "#FF5555");
|
|
||||||
scope.push_constant("BrightMagenta","#FF55FF");
|
|
||||||
scope.push_constant("Yellow", "#FFFF55");
|
|
||||||
scope.push_constant("White", "#FFFFFF");
|
|
||||||
scope
|
scope
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ fn board_with_object(object_script: Option<&str>, scripts: &[(&str, &str)]) -> (
|
|||||||
objects: BTreeMap::from([(1, object)]),
|
objects: BTreeMap::from([(1, object)]),
|
||||||
next_object_id: 2,
|
next_object_id: 2,
|
||||||
portals: Vec::new(),
|
portals: Vec::new(),
|
||||||
font: None,
|
|
||||||
zoom: 1,
|
|
||||||
board_script_name: None,
|
board_script_name: None,
|
||||||
load_errors: Vec::new(),
|
load_errors: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ pub enum Solid {
|
|||||||
///
|
///
|
||||||
/// **Not yet runtime-wired.** Portal navigation (multi-board loading and
|
/// **Not yet runtime-wired.** Portal navigation (multi-board loading and
|
||||||
/// switching) is a future feature.
|
/// switching) is a future feature.
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct PortalDef {
|
pub struct PortalDef {
|
||||||
/// Column of this portal on the board (0-indexed).
|
/// Column of this portal on the board (0-indexed).
|
||||||
pub x: usize,
|
pub x: usize,
|
||||||
|
|||||||
+15
-3
@@ -1,6 +1,6 @@
|
|||||||
[world]
|
[world]
|
||||||
name = "Kiln Demo"
|
name = "Kiln Demo"
|
||||||
start = "house"
|
start = "start"
|
||||||
|
|
||||||
# Named Rhai scripts shared across all boards in this world.
|
# Named Rhai scripts shared across all boards in this world.
|
||||||
# Objects reference a script by name via `script_name`.
|
# Objects reference a script by name via `script_name`.
|
||||||
@@ -26,9 +26,21 @@ mover = """
|
|||||||
// tick() runs. Queue.length() avoids piling up moves; blocked() avoids walking
|
// tick() runs. Queue.length() avoids piling up moves; blocked() avoids walking
|
||||||
// into a wall (or another object's already-queued move this tick).
|
// into a wall (or another object's already-queued move this tick).
|
||||||
fn tick(dt) {
|
fn tick(dt) {
|
||||||
if Queue.length() == 0 {
|
if Queue.length() == 0 { init(); }
|
||||||
move(East);
|
|
||||||
}
|
}
|
||||||
|
fn init() {
|
||||||
|
move(East);
|
||||||
|
move(South);
|
||||||
|
move(North);
|
||||||
|
move(East);
|
||||||
|
say("Hey!", 0.5);
|
||||||
|
delay(0.5);
|
||||||
|
move(West);
|
||||||
|
move(South);
|
||||||
|
move(North);
|
||||||
|
move(West);
|
||||||
|
say("Ho!", 0.5);
|
||||||
|
delay(0.5);
|
||||||
}
|
}
|
||||||
// Fires when something steps into this object; id is the bumper's object id,
|
// Fires when something steps into this object; id is the bumper's object id,
|
||||||
// or -1 for the player.
|
// or -1 for the player.
|
||||||
|
|||||||
Reference in New Issue
Block a user