Block scripts and triggers: data model, Rhai translation and runtime
Editor missions keep a painted map or roll a random map per campaign run, leave out goals they cannot have, and run their triggers in the fixed step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5315709776
commit
5701124c4f
13 files changed
+1835
-483
No files matched your search
+533
@@ -0,0 +1,533 @@
|
||||
// Mission scripts as blocks: the block editor builds these trees, they are stored in the
|
||||
// campaign file and translated into Rhai (internally only; the player never sees code).
|
||||
//
|
||||
// A script is a list of statement blocks. Values (numbers, yes/no conditions) are small
|
||||
// expression trees. Every block has a fixed shape, so the editor can draw and check it.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// side numbers in blocks: -1 any, 0 player, 1 heroes, 2 Mossgrim, 3 Lady Nightwell, 4 rival 3
|
||||
pub const SIDES: [i32; 6] = [-1, 0, 1, 2, 3, 4];
|
||||
|
||||
// creature and hero kinds a block may name ("" = any)
|
||||
pub const KINDS: [&str; 22] = [
|
||||
"Worker", "Kobold", "Spider", "Mage", "Ogre", "Jailer", "Priestess", "Chainbreaker", "Bat", "Gargoyle", "Owl", "Wyrm", "Cogling", "Revenant", "Knight", "Archer", "Priest", "Dwarf", "Paladin", "Giant", "Fairy",
|
||||
"BattleMage",
|
||||
];
|
||||
|
||||
// ground a Terrain block can make
|
||||
pub const TERRAIN: [&str; 6] = ["floor", "earth", "rock", "water", "lava", "gold"];
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "b")]
|
||||
pub enum Block {
|
||||
Message { text: String },
|
||||
Objective { text: String },
|
||||
Hint { text: String },
|
||||
Spawn { side: i32, kind: String, count: Val, level: Val, place: Place, #[serde(default)] name: String },
|
||||
Wave { count: Val },
|
||||
Gold { amount: Val },
|
||||
Mana { amount: Val },
|
||||
SetVar { name: String, value: Val },
|
||||
AddVar { name: String, value: Val },
|
||||
If { cond: Val, then: Vec<Block>, #[serde(default)] els: Vec<Block> },
|
||||
Repeat { times: Val, body: Vec<Block> },
|
||||
Win,
|
||||
Lose,
|
||||
Reveal { zone: String },
|
||||
Camera { zone: String },
|
||||
Terrain { zone: String, kind: String },
|
||||
Kill { zone: String, side: i32 },
|
||||
Trigger { name: String, on: bool },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "p", content = "zone")]
|
||||
pub enum Place {
|
||||
Zone(String),
|
||||
Event, // where the unit that set off the trigger is
|
||||
Core, // next to the player's soul core
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "v")]
|
||||
pub enum Val {
|
||||
Num { n: f64 },
|
||||
Bool { b: bool },
|
||||
Var { name: String },
|
||||
Gold,
|
||||
Mana,
|
||||
Time,
|
||||
Creatures { side: i32, kind: String },
|
||||
InZone { zone: String, side: i32, kind: String },
|
||||
Random { a: Box<Val>, b: Box<Val> },
|
||||
Math { op: String, a: Box<Val>, b: Box<Val> },
|
||||
Cmp { op: String, a: Box<Val>, b: Box<Val> },
|
||||
And { a: Box<Val>, b: Box<Val> },
|
||||
Or { a: Box<Val>, b: Box<Val> },
|
||||
Not { a: Box<Val> },
|
||||
EventKind { kind: String }, // the unit that set off the trigger is of this kind
|
||||
}
|
||||
|
||||
pub const MATH_OPS: [&str; 4] = ["+", "-", "*", "/"];
|
||||
pub const CMP_OPS: [&str; 6] = ["<", "<=", "=", "!=", ">=", ">"];
|
||||
|
||||
// what a value slot expects
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Ty {
|
||||
Num,
|
||||
Bool,
|
||||
}
|
||||
|
||||
impl Val {
|
||||
pub fn num(n: f64) -> Val {
|
||||
Val::Num { n }
|
||||
}
|
||||
|
||||
pub fn ty(&self) -> Ty {
|
||||
match self {
|
||||
Val::Bool { .. } | Val::Cmp { .. } | Val::And { .. } | Val::Or { .. } | Val::Not { .. } | Val::EventKind { .. } => Ty::Bool,
|
||||
_ => Ty::Num,
|
||||
}
|
||||
}
|
||||
|
||||
// a fresh value of a kind (for the block palette)
|
||||
pub fn template(kind: &str) -> Option<Val> {
|
||||
let n = |x: f64| Box::new(Val::num(x));
|
||||
Some(match kind {
|
||||
"num" => Val::num(0.0),
|
||||
"bool" => Val::Bool { b: true },
|
||||
"var" => Val::Var { name: String::new() },
|
||||
"gold" => Val::Gold,
|
||||
"mana" => Val::Mana,
|
||||
"time" => Val::Time,
|
||||
"creatures" => Val::Creatures { side: 0, kind: String::new() },
|
||||
"inzone" => Val::InZone { zone: String::new(), side: -1, kind: String::new() },
|
||||
"random" => Val::Random { a: n(1.0), b: n(6.0) },
|
||||
"math" => Val::Math { op: "+".into(), a: n(0.0), b: n(1.0) },
|
||||
"cmp" => Val::Cmp { op: ">".into(), a: Box::new(Val::Gold), b: n(1000.0) },
|
||||
"and" => Val::And { a: Box::new(Val::Bool { b: true }), b: Box::new(Val::Bool { b: true }) },
|
||||
"or" => Val::Or { a: Box::new(Val::Bool { b: true }), b: Box::new(Val::Bool { b: false }) },
|
||||
"not" => Val::Not { a: Box::new(Val::Bool { b: true }) },
|
||||
"eventkind" => Val::EventKind { kind: "Knight".into() },
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
// the palette name of this value's kind (see template)
|
||||
pub fn kind_name(&self) -> &'static str {
|
||||
match self {
|
||||
Val::Num { .. } => "num",
|
||||
Val::Bool { .. } => "bool",
|
||||
Val::Var { .. } => "var",
|
||||
Val::Gold => "gold",
|
||||
Val::Mana => "mana",
|
||||
Val::Time => "time",
|
||||
Val::Creatures { .. } => "creatures",
|
||||
Val::InZone { .. } => "inzone",
|
||||
Val::Random { .. } => "random",
|
||||
Val::Math { .. } => "math",
|
||||
Val::Cmp { .. } => "cmp",
|
||||
Val::And { .. } => "and",
|
||||
Val::Or { .. } => "or",
|
||||
Val::Not { .. } => "not",
|
||||
Val::EventKind { .. } => "eventkind",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// value kinds by type, for the value picker
|
||||
pub const NUM_VALUES: [&str; 9] = ["num", "var", "gold", "mana", "time", "creatures", "inzone", "random", "math"];
|
||||
pub const BOOL_VALUES: [&str; 6] = ["cmp", "and", "or", "not", "bool", "eventkind"];
|
||||
|
||||
// statement blocks in the palette: (category, kind)
|
||||
pub const PALETTE: [(&str, &str); 18] = [
|
||||
("say", "message"),
|
||||
("say", "objective"),
|
||||
("say", "hint"),
|
||||
("units", "spawn"),
|
||||
("units", "wave"),
|
||||
("units", "kill"),
|
||||
("world", "terrain"),
|
||||
("world", "reveal"),
|
||||
("world", "camera"),
|
||||
("world", "gold"),
|
||||
("world", "mana"),
|
||||
("logic", "if"),
|
||||
("logic", "repeat"),
|
||||
("logic", "trigger"),
|
||||
("vars", "setvar"),
|
||||
("vars", "addvar"),
|
||||
("mission", "win"),
|
||||
("mission", "lose"),
|
||||
];
|
||||
|
||||
pub const CATEGORIES: [&str; 6] = ["say", "units", "world", "logic", "vars", "mission"];
|
||||
|
||||
impl Block {
|
||||
pub fn template(kind: &str) -> Option<Block> {
|
||||
let n = |x: f64| Val::num(x);
|
||||
Some(match kind {
|
||||
"message" => Block::Message { text: String::new() },
|
||||
"objective" => Block::Objective { text: String::new() },
|
||||
"hint" => Block::Hint { text: String::new() },
|
||||
"spawn" => Block::Spawn { side: 1, kind: "Knight".into(), count: n(1.0), level: n(1.0), place: Place::Core, name: String::new() },
|
||||
"wave" => Block::Wave { count: n(1.0) },
|
||||
"kill" => Block::Kill { zone: String::new(), side: 1 },
|
||||
"terrain" => Block::Terrain { zone: String::new(), kind: "floor".into() },
|
||||
"reveal" => Block::Reveal { zone: String::new() },
|
||||
"camera" => Block::Camera { zone: String::new() },
|
||||
"gold" => Block::Gold { amount: n(500.0) },
|
||||
"mana" => Block::Mana { amount: n(100.0) },
|
||||
"if" => Block::If { cond: Val::template("cmp").unwrap(), then: Vec::new(), els: Vec::new() },
|
||||
"repeat" => Block::Repeat { times: n(3.0), body: Vec::new() },
|
||||
"trigger" => Block::Trigger { name: String::new(), on: true },
|
||||
"setvar" => Block::SetVar { name: String::new(), value: n(0.0) },
|
||||
"addvar" => Block::AddVar { name: String::new(), value: n(1.0) },
|
||||
"win" => Block::Win,
|
||||
"lose" => Block::Lose,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn kind_name(&self) -> &'static str {
|
||||
match self {
|
||||
Block::Message { .. } => "message",
|
||||
Block::Objective { .. } => "objective",
|
||||
Block::Hint { .. } => "hint",
|
||||
Block::Spawn { .. } => "spawn",
|
||||
Block::Wave { .. } => "wave",
|
||||
Block::Kill { .. } => "kill",
|
||||
Block::Terrain { .. } => "terrain",
|
||||
Block::Reveal { .. } => "reveal",
|
||||
Block::Camera { .. } => "camera",
|
||||
Block::Gold { .. } => "gold",
|
||||
Block::Mana { .. } => "mana",
|
||||
Block::If { .. } => "if",
|
||||
Block::Repeat { .. } => "repeat",
|
||||
Block::Trigger { .. } => "trigger",
|
||||
Block::SetVar { .. } => "setvar",
|
||||
Block::AddVar { .. } => "addvar",
|
||||
Block::Win => "win",
|
||||
Block::Lose => "lose",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn category(&self) -> &'static str {
|
||||
let k = self.kind_name();
|
||||
PALETTE.iter().find(|(_, b)| *b == k).map_or("logic", |(c, _)| c)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ checking
|
||||
|
||||
// what a script may refer to
|
||||
pub struct Names<'a> {
|
||||
pub zones: &'a [String],
|
||||
pub triggers: &'a [String],
|
||||
pub vars: &'a [String], // variables set somewhere in the mission
|
||||
}
|
||||
|
||||
// a problem in a script: the path to the block (indices into nested lists) and a language key
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Problem {
|
||||
pub path: Vec<usize>,
|
||||
pub key: &'static str,
|
||||
}
|
||||
|
||||
fn check_val(v: &Val, want: Ty, n: &Names, path: &[usize], out: &mut Vec<Problem>) {
|
||||
let p = |key| Problem { path: path.to_vec(), key };
|
||||
if v.ty() != want {
|
||||
out.push(p("blk.err_type"));
|
||||
}
|
||||
match v {
|
||||
Val::Var { name } if name.is_empty() => out.push(p("blk.err_var")),
|
||||
Val::Var { name } if !n.vars.iter().any(|x| x == name) => out.push(p("blk.err_var_unset")),
|
||||
Val::Creatures { kind, .. } if !kind.is_empty() && !KINDS.contains(&kind.as_str()) => out.push(p("blk.err_kind")),
|
||||
Val::InZone { zone, .. } if !n.zones.iter().any(|z| z == zone) => out.push(p("blk.err_zone")),
|
||||
Val::EventKind { kind } if !KINDS.contains(&kind.as_str()) => out.push(p("blk.err_kind")),
|
||||
Val::Random { a, b } | Val::Math { a, b, .. } => {
|
||||
check_val(a, Ty::Num, n, path, out);
|
||||
check_val(b, Ty::Num, n, path, out);
|
||||
}
|
||||
Val::Cmp { a, b, .. } => {
|
||||
check_val(a, Ty::Num, n, path, out);
|
||||
check_val(b, Ty::Num, n, path, out);
|
||||
}
|
||||
Val::And { a, b } | Val::Or { a, b } => {
|
||||
check_val(a, Ty::Bool, n, path, out);
|
||||
check_val(b, Ty::Bool, n, path, out);
|
||||
}
|
||||
Val::Not { a } => check_val(a, Ty::Bool, n, path, out),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_list(list: &[Block], n: &Names, path: &mut Vec<usize>, out: &mut Vec<Problem>) {
|
||||
for (k, b) in list.iter().enumerate() {
|
||||
path.push(k);
|
||||
let p = |key, path: &Vec<usize>| Problem { path: path.clone(), key };
|
||||
let zone_ok = |z: &String| n.zones.iter().any(|x| x == z);
|
||||
match b {
|
||||
Block::Message { text } | Block::Objective { text } | Block::Hint { text } if text.trim().is_empty() => out.push(p("blk.err_text", path)),
|
||||
Block::Spawn { kind, count, level, place, .. } => {
|
||||
if !KINDS.contains(&kind.as_str()) {
|
||||
out.push(p("blk.err_kind", path));
|
||||
}
|
||||
check_val(count, Ty::Num, n, path, out);
|
||||
check_val(level, Ty::Num, n, path, out);
|
||||
if let Place::Zone(z) = place {
|
||||
if !zone_ok(z) {
|
||||
out.push(p("blk.err_zone", path));
|
||||
}
|
||||
}
|
||||
}
|
||||
Block::Wave { count } => check_val(count, Ty::Num, n, path, out),
|
||||
Block::Gold { amount } | Block::Mana { amount } => check_val(amount, Ty::Num, n, path, out),
|
||||
Block::SetVar { name, value } | Block::AddVar { name, value } => {
|
||||
if !valid_var(name) {
|
||||
out.push(p("blk.err_var", path));
|
||||
}
|
||||
check_val(value, Ty::Num, n, path, out);
|
||||
}
|
||||
Block::If { cond, then, els } => {
|
||||
check_val(cond, Ty::Bool, n, path, out);
|
||||
path.push(0);
|
||||
check_list(then, n, path, out);
|
||||
path.pop();
|
||||
path.push(1);
|
||||
check_list(els, n, path, out);
|
||||
path.pop();
|
||||
}
|
||||
Block::Repeat { times, body } => {
|
||||
check_val(times, Ty::Num, n, path, out);
|
||||
path.push(0);
|
||||
check_list(body, n, path, out);
|
||||
path.pop();
|
||||
}
|
||||
Block::Reveal { zone } | Block::Camera { zone } | Block::Kill { zone, .. } if !zone_ok(zone) => out.push(p("blk.err_zone", path)),
|
||||
Block::Terrain { zone, kind } => {
|
||||
if !zone_ok(zone) {
|
||||
out.push(p("blk.err_zone", path));
|
||||
}
|
||||
if !TERRAIN.contains(&kind.as_str()) {
|
||||
out.push(p("blk.err_terrain", path));
|
||||
}
|
||||
}
|
||||
Block::Trigger { name, .. } if !n.triggers.iter().any(|t| t == name) => out.push(p("blk.err_trigger", path)),
|
||||
_ => {}
|
||||
}
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(list: &[Block], n: &Names) -> Vec<Problem> {
|
||||
let mut out = Vec::new();
|
||||
check_list(list, n, &mut Vec::new(), &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
// variable names: letters, digits and _, not starting with a digit
|
||||
pub fn valid_var(name: &str) -> bool {
|
||||
!name.is_empty() && name.len() <= 24 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') && !name.chars().next().unwrap().is_ascii_digit()
|
||||
}
|
||||
|
||||
// every variable a list of blocks sets
|
||||
pub fn vars_set(list: &[Block], out: &mut Vec<String>) {
|
||||
for b in list {
|
||||
match b {
|
||||
Block::SetVar { name, .. } | Block::AddVar { name, .. } => {
|
||||
if valid_var(name) && !out.contains(name) {
|
||||
out.push(name.clone());
|
||||
}
|
||||
}
|
||||
Block::If { then, els, .. } => {
|
||||
vars_set(then, out);
|
||||
vars_set(els, out);
|
||||
}
|
||||
Block::Repeat { body, .. } => vars_set(body, out),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Rhai code
|
||||
|
||||
fn q(s: &str) -> String {
|
||||
let mut o = String::from("\"");
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'"' => o.push_str("\\\""),
|
||||
'\\' => o.push_str("\\\\"),
|
||||
'\n' => o.push_str("\\n"),
|
||||
'\r' | '\t' => o.push(' '),
|
||||
c => o.push(c),
|
||||
}
|
||||
}
|
||||
o.push('"');
|
||||
o
|
||||
}
|
||||
|
||||
fn f(n: f64) -> String {
|
||||
if n.is_finite() {
|
||||
format!("{:?}", n) // always with a decimal point: Rhai keeps it a float
|
||||
} else {
|
||||
"0.0".into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn val_code(v: &Val) -> String {
|
||||
match v {
|
||||
Val::Num { n } => f(*n),
|
||||
Val::Bool { b } => b.to_string(),
|
||||
Val::Var { name } => format!("m.get({})", q(name)),
|
||||
Val::Gold => "m.gold()".into(),
|
||||
Val::Mana => "m.mana()".into(),
|
||||
Val::Time => "m.time()".into(),
|
||||
Val::Creatures { side, kind } => format!("m.count({}, {})", side, q(kind)),
|
||||
Val::InZone { zone, side, kind } => format!("m.in_zone({}, {}, {})", q(zone), side, q(kind)),
|
||||
Val::Random { a, b } => format!("m.random({}, {})", val_code(a), val_code(b)),
|
||||
Val::Math { op, a, b } => {
|
||||
let op = if MATH_OPS.contains(&op.as_str()) { op.as_str() } else { "+" };
|
||||
if op == "/" {
|
||||
format!("m.div({}, {})", val_code(a), val_code(b))
|
||||
} else {
|
||||
format!("({} {} {})", val_code(a), op, val_code(b))
|
||||
}
|
||||
}
|
||||
Val::Cmp { op, a, b } => {
|
||||
let op = match op.as_str() {
|
||||
"=" => "==",
|
||||
o if CMP_OPS.contains(&o) => o,
|
||||
_ => ">",
|
||||
};
|
||||
format!("({} {} {})", val_code(a), op, val_code(b))
|
||||
}
|
||||
Val::And { a, b } => format!("({} && {})", val_code(a), val_code(b)),
|
||||
Val::Or { a, b } => format!("({} || {})", val_code(a), val_code(b)),
|
||||
Val::Not { a } => format!("(!{})", val_code(a)),
|
||||
Val::EventKind { kind } => format!("m.event_is({})", q(kind)),
|
||||
}
|
||||
}
|
||||
|
||||
fn cond_code(v: &Val) -> String {
|
||||
if v.ty() == Ty::Bool {
|
||||
val_code(v)
|
||||
} else {
|
||||
format!("({} != 0.0)", val_code(v))
|
||||
}
|
||||
}
|
||||
|
||||
fn list_code(list: &[Block], depth: usize, out: &mut String) {
|
||||
let ind = " ".repeat(depth + 1);
|
||||
for b in list {
|
||||
let line = match b {
|
||||
Block::Message { text } => format!("m.message({});", q(text)),
|
||||
Block::Objective { text } => format!("m.objective({});", q(text)),
|
||||
Block::Hint { text } => format!("m.hint({});", q(text)),
|
||||
Block::Spawn { side, kind, count, level, place, name } => {
|
||||
let (pk, pz) = match place {
|
||||
Place::Zone(z) => ("zone", z.as_str()),
|
||||
Place::Event => ("event", ""),
|
||||
Place::Core => ("core", ""),
|
||||
};
|
||||
format!("m.spawn_unit({}, {}, {}, {}, {}, {}, {});", side, q(kind), val_code(count), val_code(level), q(pk), q(pz), q(name))
|
||||
}
|
||||
Block::Wave { count } => format!("m.wave({});", val_code(count)),
|
||||
Block::Gold { amount } => format!("m.add_gold({});", val_code(amount)),
|
||||
Block::Mana { amount } => format!("m.add_mana({});", val_code(amount)),
|
||||
Block::SetVar { name, value } => format!("m.set({}, {});", q(name), val_code(value)),
|
||||
Block::AddVar { name, value } => format!("m.set({}, m.get({}) + {});", q(name), q(name), val_code(value)),
|
||||
Block::If { cond, then, els } => {
|
||||
out.push_str(&format!("{}if {} {{\n", ind, cond_code(cond)));
|
||||
list_code(then, depth + 1, out);
|
||||
if !els.is_empty() {
|
||||
out.push_str(&format!("{}}} else {{\n", ind));
|
||||
list_code(els, depth + 1, out);
|
||||
}
|
||||
out.push_str(&format!("{}}}\n", ind));
|
||||
continue;
|
||||
}
|
||||
Block::Repeat { times, body } => {
|
||||
out.push_str(&format!("{}for _i{} in 0..m.times({}) {{\n", ind, depth, val_code(times)));
|
||||
list_code(body, depth + 1, out);
|
||||
out.push_str(&format!("{}}}\n", ind));
|
||||
continue;
|
||||
}
|
||||
Block::Win => "m.win();".into(),
|
||||
Block::Lose => "m.lose();".into(),
|
||||
Block::Reveal { zone } => format!("m.reveal({});", q(zone)),
|
||||
Block::Camera { zone } => format!("m.camera({});", q(zone)),
|
||||
Block::Terrain { zone, kind } => format!("m.terrain({}, {});", q(zone), q(kind)),
|
||||
Block::Kill { zone, side } => format!("m.kill({}, {});", q(zone), side),
|
||||
Block::Trigger { name, on } => format!("m.trigger({}, {});", q(name), on),
|
||||
};
|
||||
out.push_str(&ind);
|
||||
out.push_str(&line);
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// a Rhai function `fn <name>(m) { ... }` for a list of blocks
|
||||
pub fn function_code(name: &str, list: &[Block]) -> String {
|
||||
let mut out = format!("fn {}(m) {{\n", name);
|
||||
list_code(list, 0, &mut out);
|
||||
out.push_str("}\n");
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn blocks_become_rhai_code() {
|
||||
let list = vec![
|
||||
Block::Message { text: "Hello \"master\"".into() },
|
||||
Block::If {
|
||||
cond: Val::Cmp { op: "=".into(), a: Box::new(Val::Var { name: "wave".into() }), b: Box::new(Val::num(2.0)) },
|
||||
then: vec![Block::Spawn { side: 1, kind: "Knight".into(), count: Val::num(3.0), level: Val::num(2.0), place: Place::Zone("gate".into()), name: String::new() }],
|
||||
els: vec![Block::Win],
|
||||
},
|
||||
Block::Repeat { times: Val::num(2.0), body: vec![Block::AddVar { name: "wave".into(), value: Val::num(1.0) }] },
|
||||
];
|
||||
let code = function_code("t_0", &list);
|
||||
assert!(code.contains("m.message(\"Hello \\\"master\\\"\");"), "{}", code);
|
||||
assert!(code.contains("if (m.get(\"wave\") == 2.0) {"), "{}", code);
|
||||
assert!(code.contains("m.spawn_unit(1, \"Knight\", 3.0, 2.0, \"zone\", \"gate\", \"\");"), "{}", code);
|
||||
assert!(code.contains("} else {"));
|
||||
assert!(code.contains("for _i0 in 0..m.times(2.0) {"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checking_finds_missing_zones_vars_and_wrong_types() {
|
||||
let zones = vec!["gate".to_string()];
|
||||
let names = Names { zones: &zones, triggers: &[], vars: &[] };
|
||||
let list = vec![
|
||||
Block::Reveal { zone: "nowhere".into() },
|
||||
Block::If { cond: Val::num(1.0), then: vec![Block::Message { text: " ".into() }], els: vec![] },
|
||||
Block::Gold { amount: Val::Var { name: "loot".into() } },
|
||||
];
|
||||
let p = check(&list, &names);
|
||||
assert!(p.contains(&Problem { path: vec![0], key: "blk.err_zone" }));
|
||||
assert!(p.contains(&Problem { path: vec![1], key: "blk.err_type" }));
|
||||
assert!(p.contains(&Problem { path: vec![1, 0, 0], key: "blk.err_text" }));
|
||||
assert!(p.contains(&Problem { path: vec![2], key: "blk.err_var_unset" }));
|
||||
let ok = vec![Block::Reveal { zone: "gate".into() }];
|
||||
assert!(check(&ok, &names).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_palette_entry_has_a_template_and_round_trips() {
|
||||
for (_, k) in PALETTE {
|
||||
let b = Block::template(k).unwrap_or_else(|| panic!("{}", k));
|
||||
assert_eq!(b.kind_name(), k);
|
||||
let json = serde_json::to_string(&b).unwrap();
|
||||
assert_eq!(serde_json::from_str::<Block>(&json).unwrap(), b);
|
||||
}
|
||||
for k in NUM_VALUES.iter().chain(BOOL_VALUES.iter()) {
|
||||
let v = Val::template(k).unwrap();
|
||||
assert_eq!(v.kind_name(), *k);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user