Wait and both kinds of until loops; blocks run directly

Script runs are resumable threads saved with the game; Rhai is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 18:57:36 +01:00
1 parent 95cf3ce81c
commit d17668f3b2
13 files changed
+466 -686

No files matched your search

+22 -147
View File
@@ -1,5 +1,5 @@
// 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).
// campaign file and run directly from the blocks (see mscript.rs); 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.
@@ -32,6 +32,9 @@ pub enum Block {
AddVar { name: String, value: Val },
If { cond: Val, then: Vec<Block>, #[serde(default)] els: Vec<Block> },
Repeat { times: Val, body: Vec<Block> },
Until { cond: Val, body: Vec<Block> }, // repeat until: checked before each run
DoUntil { cond: Val, body: Vec<Block> }, // repeat ... until: checked after each run
Wait { secs: Val },
Win,
Lose,
Reveal { zone: String },
@@ -141,7 +144,7 @@ pub const NUM_VALUES: [&str; 9] = ["num", "var", "gold", "mana", "time", "creatu
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] = [
pub const PALETTE: [(&str, &str); 21] = [
("say", "message"),
("say", "objective"),
("say", "hint"),
@@ -155,6 +158,9 @@ pub const PALETTE: [(&str, &str); 18] = [
("world", "mana"),
("logic", "if"),
("logic", "repeat"),
("logic", "until"),
("logic", "dountil"),
("logic", "wait"),
("logic", "trigger"),
("vars", "setvar"),
("vars", "addvar"),
@@ -181,6 +187,9 @@ impl Block {
"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() },
"until" => Block::Until { cond: Val::template("cmp").unwrap(), body: Vec::new() },
"dountil" => Block::DoUntil { cond: Val::template("cmp").unwrap(), body: Vec::new() },
"wait" => Block::Wait { secs: n(5.0) },
"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) },
@@ -205,6 +214,9 @@ impl Block {
Block::Mana { .. } => "mana",
Block::If { .. } => "if",
Block::Repeat { .. } => "repeat",
Block::Until { .. } => "until",
Block::DoUntil { .. } => "dountil",
Block::Wait { .. } => "wait",
Block::Trigger { .. } => "trigger",
Block::SetVar { .. } => "setvar",
Block::AddVar { .. } => "addvar",
@@ -305,6 +317,13 @@ fn check_list(list: &[Block], n: &Names, path: &mut Vec<usize>, out: &mut Vec<Pr
check_list(body, n, path, out);
path.pop();
}
Block::Until { cond, body } | Block::DoUntil { cond, body } => {
check_val(cond, Ty::Bool, n, path, out);
path.push(0);
check_list(body, n, path, out);
path.pop();
}
Block::Wait { secs } => check_val(secs, Ty::Num, n, path, out),
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) {
@@ -345,160 +364,16 @@ pub fn vars_set(list: &[Block], out: &mut Vec<String>) {
vars_set(then, out);
vars_set(els, out);
}
Block::Repeat { body, .. } => vars_set(body, out),
Block::Repeat { body, .. } | Block::Until { body, .. } | Block::DoUntil { 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()];