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:
1 parent
95cf3ce81c
commit
d17668f3b2
13 files changed
+466
-686
No files matched your search
+22
-147
@@ -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()];
|
||||
|
||||
Reference in new issue
Block a user