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()];
|
||||
|
||||
@@ -85,7 +85,7 @@ fn list_mut<'a>(root: &'a mut Vec<Block>, lp: &[usize]) -> Option<&'a mut Vec<Bl
|
||||
els
|
||||
}
|
||||
}
|
||||
Block::Repeat { body, .. } => body,
|
||||
Block::Repeat { body, .. } | Block::Until { body, .. } | Block::DoUntil { body, .. } => body,
|
||||
_ => return None,
|
||||
};
|
||||
k += 2;
|
||||
@@ -432,7 +432,7 @@ impl BlockEdit {
|
||||
// slots: upper half before this block, lower half after it
|
||||
slots.push((Rect::new(x, y - CH() * 0.3, w, header.h * 0.5 + CH() * 0.3), lp.to_vec(), k));
|
||||
slots.push((Rect::new(x, y + header.h * 0.5, w, header.h * 0.5), lp.to_vec(), k + 1));
|
||||
let nested = matches!(list[k], Block::If { .. } | Block::Repeat { .. });
|
||||
let nested = matches!(list[k], Block::If { .. } | Block::Repeat { .. } | Block::Until { .. } | Block::DoUntil { .. });
|
||||
// the block: header row with its fields
|
||||
draw_block_shape(header, col, selected, found);
|
||||
if let Some(p) = problem {
|
||||
@@ -479,7 +479,7 @@ impl BlockEdit {
|
||||
els
|
||||
}
|
||||
}
|
||||
Block::Repeat { body, .. } => body,
|
||||
Block::Repeat { body, .. } | Block::Until { body, .. } | Block::DoUntil { body, .. } => body,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let start = y;
|
||||
@@ -563,6 +563,11 @@ impl BlockEdit {
|
||||
self.value(im, gfx, times, Ty::Num, path, 1, cx, y, ctx);
|
||||
word(cx, &tr("blk.w_times"));
|
||||
}
|
||||
Block::Until { cond, .. } | Block::DoUntil { cond, .. } => self.value(im, gfx, cond, Ty::Bool, path, 1, cx, y, ctx),
|
||||
Block::Wait { secs } => {
|
||||
self.value(im, gfx, secs, Ty::Num, path, 1, cx, y, ctx);
|
||||
word(cx, &tr("blk.w_seconds"));
|
||||
}
|
||||
Block::Win | Block::Lose => {}
|
||||
Block::Reveal { zone } | Block::Camera { zone } => self.zone_dd(im, gfx, zone, path, 1, cx, y, ctx),
|
||||
Block::Terrain { zone, kind } => {
|
||||
|
||||
+25
-3
@@ -164,16 +164,38 @@ pub fn demo_campaign() -> CampaignDoc {
|
||||
trig("vault_claimed", TriggerKind::ZoneClaimed { zone: "vault".into() }, true, vec![say("demo.vault"), Block::Gold { amount: n(500.0) }]),
|
||||
trig("tunnel_dug", TriggerKind::ZoneDug { zone: "tunnel".into() }, true, vec![say("demo.tunnel")]),
|
||||
trig("crowd", TriggerKind::ZoneCount { zone: "field".into(), side: 1, kind: String::new(), op: ">=".into(), n: 4.0 }, true, vec![say("demo.crowd"), Block::Camera { zone: "field".into() }]),
|
||||
trig("reinforcements", TriggerKind::Timer { secs: 90.0, every: false }, true, vec![say("demo.wave"), Block::Spawn { side: 1, kind: "Archer".into(), count: n(2.0), level: n(2.0), place: Place::Zone("field".into()), name: String::new() }]),
|
||||
// repeat until (checked first): one archer every 15 seconds until four heroes stand on the field
|
||||
trig(
|
||||
"reinforcements",
|
||||
TriggerKind::Timer { secs: 90.0, every: false },
|
||||
true,
|
||||
vec![
|
||||
say("demo.wave"),
|
||||
Block::Until {
|
||||
cond: cmp(">=", Val::InZone { zone: "field".into(), side: 1, kind: String::new() }, n(4.0)),
|
||||
body: vec![Block::Spawn { side: 1, kind: "Archer".into(), count: n(1.0), level: n(2.0), place: Place::Zone("field".into()), name: String::new() }, Block::Wait { secs: n(15.0) }],
|
||||
},
|
||||
],
|
||||
),
|
||||
trig("clock", TriggerKind::Timer { secs: 30.0, every: true }, false, vec![Block::AddVar { name: "ticks".into(), value: n(1.0) }]),
|
||||
trig("knight_falls", TriggerKind::UnitDies { side: 1, kind: String::new(), name: String::new() }, false, vec![Block::AddVar { name: "kills".into(), value: n(1.0) }, Block::Message { text: tr("demo.kills") }]),
|
||||
trig("captain_falls", TriggerKind::UnitDies { side: 1, kind: String::new(), name: "captain".into() }, true, vec![say("demo.captain"), Block::Mana { amount: n(50.0) }]),
|
||||
trig("captain_falls", TriggerKind::UnitDies { side: 1, kind: String::new(), name: "captain".into() }, true, vec![say("demo.captain"), Block::Repeat { times: n(5.0), body: vec![Block::Mana { amount: n(10.0) }] }]),
|
||||
trig("tower_down", TriggerKind::TowerFalls { side: 1 }, false, vec![say("demo.tower"), Block::Gold { amount: Val::Random { a: Box::new(n(100.0)), b: Box::new(n(300.0)) } }]),
|
||||
trig("rich", TriggerKind::Value { what: "gold".into(), var: String::new(), op: ">=".into(), n: 8000.0 }, true, vec![say("demo.rich")]),
|
||||
trig("lair", TriggerKind::RoomBuilt { room: "Lair".into() }, true, vec![Block::Hint { text: tr("demo.lair") }]),
|
||||
trig("spell", TriggerKind::SpellCast { spell: String::new() }, false, vec![Block::Mana { amount: n(10.0) }]),
|
||||
trig("slap", TriggerKind::Slap, true, vec![say("demo.slap")]),
|
||||
trig("research", TriggerKind::Research, true, vec![say("demo.research"), Block::Repeat { times: n(2.0), body: vec![Block::Gold { amount: n(100.0) }] }]),
|
||||
// repeat ... until (checked after): a little gold three times, a second apart
|
||||
trig(
|
||||
"research",
|
||||
TriggerKind::Research,
|
||||
true,
|
||||
vec![
|
||||
say("demo.research"),
|
||||
Block::SetVar { name: "bonus".into(), value: n(0.0) },
|
||||
Block::DoUntil { cond: cmp(">=", var("bonus"), n(3.0)), body: vec![Block::Gold { amount: n(100.0) }, Block::AddVar { name: "bonus".into(), value: n(1.0) }, Block::Wait { secs: n(1.0) }] },
|
||||
],
|
||||
),
|
||||
];
|
||||
doc.missions.push(m);
|
||||
|
||||
|
||||
+353
-187
@@ -1,24 +1,23 @@
|
||||
// Mission scripts at run time: the block scripts of a mission (campaign_doc::MissionScripts)
|
||||
// become one Rhai program; the triggers are checked ten times a second inside the fixed
|
||||
// simulation step, so a co-op match runs them identically on every computer.
|
||||
//
|
||||
// The program never touches the game directly: reading functions (m.gold(), m.in_zone(..))
|
||||
// look at the game through a pointer that is valid while a script runs, and everything a
|
||||
// script does is collected as actions that are applied afterwards.
|
||||
// are run directly from their blocks inside the fixed simulation step, so a co-op match runs
|
||||
// them identically on every computer. Every run of a script is a small "thread" (where it is
|
||||
// in which list of blocks); "wait" parks a thread until its time has come, and threads are
|
||||
// saved with the game. Loops without a wait run up to a budget of steps per check and go on at
|
||||
// the next check, so a script can never hang the game.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use rhai::{Engine, Scope, AST};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::blocks::function_code;
|
||||
use crate::blocks::{Block, Place, Val};
|
||||
use crate::campaign_doc::{MissionScripts, TriggerKind, ZoneDoc};
|
||||
use crate::game::*;
|
||||
use crate::map::*;
|
||||
|
||||
const CHECK_STEPS: u64 = 5; // triggers are checked every 5 fixed steps (0.1 s)
|
||||
const MAX_LOG: usize = 60;
|
||||
const STEP_BUDGET: u32 = 2000; // blocks a thread may run per check before it yields
|
||||
const MAX_THREADS: usize = 64;
|
||||
|
||||
// pending events the simulation reports (the rest is found by comparing two checks)
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -59,10 +58,40 @@ pub struct ScriptState {
|
||||
next_second: f32,
|
||||
rng: u32,
|
||||
pub error: Option<String>,
|
||||
#[serde(default)]
|
||||
threads: Vec<Thread>, // scripts that are waiting (or yielded in a long loop)
|
||||
#[serde(skip)]
|
||||
pub camera: Option<(f32, f32)>, // a block asked the camera to look here (taken by main)
|
||||
}
|
||||
|
||||
// which script a thread runs
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||
enum Src {
|
||||
Start,
|
||||
Victory,
|
||||
Defeat,
|
||||
Tick,
|
||||
Trigger(usize),
|
||||
}
|
||||
|
||||
// a list of blocks being worked through: its path (pairs of block index and branch below the
|
||||
// script's own list), the next block, and what kind of list it is
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct Frame {
|
||||
lp: Vec<usize>,
|
||||
idx: usize,
|
||||
left: i64, // repeat: runs still to come after this one (-1: not a repeat)
|
||||
loop_kind: u8, // 0 plain, 1 repeat, 2 repeat until (checked before), 3 repeat ... until (checked after)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct Thread {
|
||||
src: Src,
|
||||
frames: Vec<Frame>,
|
||||
wake: f32,
|
||||
ev: EventInfo,
|
||||
}
|
||||
|
||||
// what a running script asks for
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Act {
|
||||
@@ -83,44 +112,12 @@ enum Act {
|
||||
}
|
||||
|
||||
// the triggering unit (for "where the unit is" and "the unit is a ...")
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
struct EventInfo {
|
||||
kind: Option<EK>,
|
||||
pos: Option<(f32, f32)>,
|
||||
}
|
||||
|
||||
struct Ctx {
|
||||
game: *const Game,
|
||||
vars: BTreeMap<String, f64>,
|
||||
acts: Vec<Act>,
|
||||
event: EventInfo,
|
||||
rng: u32,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static CTX: RefCell<Ctx> = RefCell::new(Ctx { game: std::ptr::null(), vars: BTreeMap::new(), acts: Vec::new(), event: EventInfo::default(), rng: 1 });
|
||||
// the compiled program of the running mission (key: the program text)
|
||||
static PROGRAM: RefCell<Option<(String, Engine, AST)>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
// reads the game while a script runs (the pointer is set by run_fn around every call)
|
||||
fn with_game<R>(f: impl FnOnce(&Game) -> R, fallback: R) -> R {
|
||||
CTX.with(|c| {
|
||||
let p = c.borrow().game;
|
||||
if p.is_null() {
|
||||
fallback
|
||||
} else {
|
||||
// SAFETY: run_fn sets the pointer from a live &Game for the duration of the call
|
||||
// and clears it afterwards; scripts run on this thread only
|
||||
f(unsafe { &*p })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn act(a: Act) {
|
||||
CTX.with(|c| c.borrow_mut().acts.push(a));
|
||||
}
|
||||
|
||||
pub fn kind_by_name(name: &str) -> Option<EK> {
|
||||
let n = name.trim().to_lowercase();
|
||||
crate::blocks::KINDS.iter().position(|k| k.to_lowercase() == n).map(|i| match i {
|
||||
@@ -247,118 +244,41 @@ fn line_side(z: &ZoneDoc, x: f32, y: f32) -> i8 {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ the program
|
||||
|
||||
// the Rhai program of a mission: one function per script and trigger
|
||||
pub fn program(s: &MissionScripts) -> String {
|
||||
let mut code = String::new();
|
||||
code.push_str(&function_code("s_start", &s.start));
|
||||
code.push_str(&function_code("s_victory", &s.victory));
|
||||
code.push_str(&function_code("s_defeat", &s.defeat));
|
||||
code.push_str(&function_code("s_tick", &s.tick));
|
||||
for (k, t) in s.triggers.iter().enumerate() {
|
||||
code.push_str(&function_code(&format!("t_{}", k), &t.script));
|
||||
}
|
||||
code
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct M;
|
||||
|
||||
fn engine() -> Engine {
|
||||
let mut e = Engine::new();
|
||||
e.set_max_operations(100_000);
|
||||
e.set_max_call_levels(16);
|
||||
e.register_type_with_name::<M>("mission");
|
||||
// reading
|
||||
e.register_fn("gold", |_: &mut M| with_game(|g| g.gold as f64, 0.0));
|
||||
e.register_fn("mana", |_: &mut M| with_game(|g| g.mana as f64, 0.0));
|
||||
e.register_fn("time", |_: &mut M| with_game(|g| g.time as f64, 0.0));
|
||||
e.register_fn("get", |_: &mut M, name: &str| CTX.with(|c| c.borrow().vars.get(name).copied().unwrap_or(0.0)));
|
||||
e.register_fn("set", |_: &mut M, name: &str, v: f64| {
|
||||
CTX.with(|c| {
|
||||
c.borrow_mut().vars.insert(name.to_string(), if v.is_finite() { v } else { 0.0 });
|
||||
})
|
||||
});
|
||||
e.register_fn("count", |_: &mut M, side: i64, kind: &str| {
|
||||
with_game(|g| g.ents.iter().filter(|e| e.alive && e.captive == 0 && !e.chained && matches(e.side(), e.kind, side as i32, kind)).count() as f64, 0.0)
|
||||
});
|
||||
e.register_fn("in_zone", |_: &mut M, zone: &str, side: i64, kind: &str| {
|
||||
with_game(
|
||||
|g| {
|
||||
let Some(z) = g.script.as_ref().and_then(|s| s.zones.iter().find(|z| z.name == zone)) else { return 0.0 };
|
||||
g.ents.iter().filter(|e| e.alive && !e.held && e.captive == 0 && matches(e.side(), e.kind, side as i32, kind) && zone_contains(z, e.x, e.y)).count() as f64
|
||||
},
|
||||
0.0,
|
||||
)
|
||||
});
|
||||
e.register_fn("random", |_: &mut M, a: f64, b: f64| {
|
||||
CTX.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
// xorshift: deterministic, part of the saved script state
|
||||
let mut x = c.rng.max(1);
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
c.rng = x;
|
||||
let (lo, hi) = (a.min(b).round(), a.max(b).round());
|
||||
lo + (x as f64 % (hi - lo + 1.0)).floor()
|
||||
})
|
||||
});
|
||||
e.register_fn("div", |_: &mut M, a: f64, b: f64| if b == 0.0 { 0.0 } else { a / b });
|
||||
e.register_fn("times", |_: &mut M, n: f64| if n.is_finite() { (n.round() as i64).clamp(0, 100) } else { 0 });
|
||||
e.register_fn("event_is", |_: &mut M, kind: &str| CTX.with(|c| c.borrow().event.kind.is_some() && c.borrow().event.kind == kind_by_name(kind)));
|
||||
// doing
|
||||
e.register_fn("message", |_: &mut M, s: &str| act(Act::Message(s.to_string())));
|
||||
e.register_fn("objective", |_: &mut M, s: &str| act(Act::Objective(s.to_string())));
|
||||
e.register_fn("hint", |_: &mut M, s: &str| act(Act::Hint(s.to_string())));
|
||||
e.register_fn("spawn_unit", |_: &mut M, side: i64, kind: &str, count: f64, level: f64, place: &str, zone: &str, name: &str| {
|
||||
act(Act::Spawn { side: side as i32, kind: kind.to_string(), count: count.round() as i64, level: level.round() as i64, place: place.to_string(), zone: zone.to_string(), name: name.to_string() })
|
||||
});
|
||||
e.register_fn("wave", |_: &mut M, n: f64| act(Act::Wave(n.round() as i64)));
|
||||
e.register_fn("add_gold", |_: &mut M, n: f64| act(Act::Gold(n)));
|
||||
e.register_fn("add_mana", |_: &mut M, n: f64| act(Act::Mana(n)));
|
||||
e.register_fn("win", |_: &mut M| act(Act::Win));
|
||||
e.register_fn("lose", |_: &mut M| act(Act::Lose));
|
||||
e.register_fn("reveal", |_: &mut M, z: &str| act(Act::Reveal(z.to_string())));
|
||||
e.register_fn("camera", |_: &mut M, z: &str| act(Act::Camera(z.to_string())));
|
||||
e.register_fn("terrain", |_: &mut M, z: &str, k: &str| act(Act::Terrain(z.to_string(), k.to_string())));
|
||||
e.register_fn("kill", |_: &mut M, z: &str, side: i64| act(Act::Kill(z.to_string(), side as i32)));
|
||||
e.register_fn("trigger", |_: &mut M, name: &str, on: bool| act(Act::Trigger(name.to_string(), on)));
|
||||
e
|
||||
}
|
||||
|
||||
// runs one function of the mission's program; returns the actions or an error
|
||||
fn run_fn(g: &Game, code: &str, name: &str, vars: &mut BTreeMap<String, f64>, rng: &mut u32, event: EventInfo) -> Result<Vec<Act>, String> {
|
||||
PROGRAM.with(|p| {
|
||||
let mut p = p.borrow_mut();
|
||||
if p.as_ref().map_or(true, |(c, _, _)| c != code) {
|
||||
let e = engine();
|
||||
let ast = e.compile(code).map_err(|e| e.to_string())?;
|
||||
*p = Some((code.to_string(), e, ast));
|
||||
}
|
||||
let (_, e, ast) = p.as_ref().unwrap();
|
||||
CTX.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
c.game = g as *const Game;
|
||||
c.vars = std::mem::take(vars);
|
||||
c.acts.clear();
|
||||
c.event = event;
|
||||
c.rng = *rng;
|
||||
});
|
||||
let mut scope = Scope::new();
|
||||
let r = e.call_fn::<rhai::Dynamic>(&mut scope, ast, name, (M,));
|
||||
CTX.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
c.game = std::ptr::null();
|
||||
*vars = std::mem::take(&mut c.vars);
|
||||
*rng = c.rng;
|
||||
match r {
|
||||
Ok(_) => Ok(std::mem::take(&mut c.acts)),
|
||||
Err(err) => Err(err.to_string()),
|
||||
// the list of blocks at a path inside a script
|
||||
fn list_at<'a>(s: &'a MissionScripts, src: Src, lp: &[usize]) -> Option<&'a [Block]> {
|
||||
let mut cur: &[Block] = match src {
|
||||
Src::Start => &s.start,
|
||||
Src::Victory => &s.victory,
|
||||
Src::Defeat => &s.defeat,
|
||||
Src::Tick => &s.tick,
|
||||
Src::Trigger(k) => &s.triggers.get(k)?.script,
|
||||
};
|
||||
let mut k = 0;
|
||||
while k + 1 < lp.len() + 1 && k < lp.len() {
|
||||
let (i, branch) = (lp[k], *lp.get(k + 1)?);
|
||||
cur = match cur.get(i)? {
|
||||
Block::If { then, els, .. } => {
|
||||
if branch == 0 {
|
||||
then
|
||||
} else {
|
||||
els
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
Block::Repeat { body, .. } | Block::Until { body, .. } | Block::DoUntil { body, .. } => body,
|
||||
_ => return None,
|
||||
};
|
||||
k += 2;
|
||||
}
|
||||
Some(cur)
|
||||
}
|
||||
|
||||
fn xorshift(rng: &mut u32) -> u32 {
|
||||
let mut x = (*rng).max(1);
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
*rng = x;
|
||||
x
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ the game side
|
||||
@@ -429,58 +349,240 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
let mut s = self.script.take().unwrap();
|
||||
let code = program(&s.scripts);
|
||||
let mut fire: Vec<(String, EventInfo, Option<usize>)> = Vec::new(); // function, event, trigger
|
||||
let mut fire: Vec<(String, EventInfo, Option<usize>)> = Vec::new();
|
||||
let mut start: Vec<(Src, EventInfo)> = Vec::new();
|
||||
if !s.started {
|
||||
s.started = true;
|
||||
s.next_second = self.time + 1.0;
|
||||
self.remember(&mut s, true);
|
||||
fire.push(("s_start".into(), EventInfo::default(), None));
|
||||
start.push((Src::Start, EventInfo::default()));
|
||||
} else {
|
||||
self.check_triggers(&mut s, &mut fire);
|
||||
if self.time >= s.next_second {
|
||||
s.next_second += 1.0;
|
||||
for f in ["s_tick", "s_victory", "s_defeat"] {
|
||||
fire.push((f.into(), EventInfo::default(), None));
|
||||
// the checks run every second, but never twice at once (one may still be waiting)
|
||||
for src in [Src::Tick, Src::Victory, Src::Defeat] {
|
||||
if !s.threads.iter().any(|t| t.src == src) {
|
||||
start.push((src, EventInfo::default()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (f, ev, trig) in fire {
|
||||
if let Some(k) = trig {
|
||||
// a trigger fires: once-triggers only the first time, switched-off ones never
|
||||
if !s.enabled.get(k).copied().unwrap_or(false) || (s.scripts.triggers[k].once && s.done[k]) {
|
||||
continue;
|
||||
}
|
||||
s.done[k] = true;
|
||||
let name = s.scripts.triggers[k].name.clone();
|
||||
s.log(self.time, name);
|
||||
for (_, ev, trig) in fire {
|
||||
let Some(k) = trig else { continue };
|
||||
// a trigger fires: once-triggers only the first time, switched-off ones never
|
||||
if !s.enabled.get(k).copied().unwrap_or(false) || (s.scripts.triggers[k].once && s.done[k]) {
|
||||
continue;
|
||||
}
|
||||
let (mut vars, mut rng) = (std::mem::take(&mut s.vars), s.rng);
|
||||
let r = run_fn(self, &code, &f, &mut vars, &mut rng, ev.clone());
|
||||
s.vars = vars;
|
||||
s.rng = rng;
|
||||
match r {
|
||||
Ok(acts) => {
|
||||
for a in acts {
|
||||
self.apply_act(&mut s, a, &ev);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if s.error.is_none() {
|
||||
self.script_msg("msg.script_error", vec![e.chars().take(120).collect()]);
|
||||
}
|
||||
s.log(self.time, format!("! {}", e.chars().take(80).collect::<String>()));
|
||||
s.error = Some(e);
|
||||
}
|
||||
}
|
||||
if self.state != 0 {
|
||||
break;
|
||||
s.done[k] = true;
|
||||
let name = s.scripts.triggers[k].name.clone();
|
||||
s.log(self.time, name);
|
||||
start.push((Src::Trigger(k), ev));
|
||||
}
|
||||
let mut threads = std::mem::take(&mut s.threads);
|
||||
for (src, ev) in start {
|
||||
threads.push(Thread { src, frames: vec![Frame { lp: Vec::new(), idx: 0, left: -1, loop_kind: 0 }], wake: 0.0, ev });
|
||||
}
|
||||
// run every thread whose time has come; the waiting ones stay
|
||||
let scripts = std::mem::take(&mut s.scripts);
|
||||
let mut keep = Vec::new();
|
||||
for mut t in threads {
|
||||
if self.state == 0 && t.wake <= self.time && !self.run_thread(&mut s, &scripts, &mut t) {
|
||||
keep.push(t);
|
||||
} else if t.wake > self.time {
|
||||
keep.push(t);
|
||||
}
|
||||
}
|
||||
if keep.len() > MAX_THREADS {
|
||||
keep.drain(0..keep.len() - MAX_THREADS);
|
||||
}
|
||||
s.scripts = scripts;
|
||||
s.threads = keep;
|
||||
s.events.clear();
|
||||
self.script = Some(s);
|
||||
}
|
||||
|
||||
// runs a thread until it ends (true), waits or used up its steps (false)
|
||||
fn run_thread(&mut self, s: &mut ScriptState, scripts: &MissionScripts, t: &mut Thread) -> bool {
|
||||
let mut budget = STEP_BUDGET;
|
||||
loop {
|
||||
if self.state != 0 {
|
||||
return true;
|
||||
}
|
||||
if budget == 0 {
|
||||
t.wake = self.time; // go on at the next check
|
||||
return false;
|
||||
}
|
||||
budget -= 1;
|
||||
let Some(fr) = t.frames.last() else { return true };
|
||||
let Some(list) = list_at(scripts, t.src, &fr.lp) else { return true };
|
||||
let (idx, lp, left, loop_kind) = (fr.idx, fr.lp.clone(), fr.left, fr.loop_kind);
|
||||
if idx >= list.len() {
|
||||
// the end of a list: loop again, or back to the list around it
|
||||
let again = match loop_kind {
|
||||
1 => left > 0,
|
||||
2 | 3 => {
|
||||
// the loop's condition sits in its block in the list around it
|
||||
let outer = list_at(scripts, t.src, &lp[..lp.len() - 2]).and_then(|l| l.get(lp[lp.len() - 2]));
|
||||
match outer {
|
||||
Some(Block::Until { cond, .. }) | Some(Block::DoUntil { cond, .. }) => !self.cond(s, cond, &t.ev),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if again {
|
||||
let fr = t.frames.last_mut().unwrap();
|
||||
fr.idx = 0;
|
||||
if loop_kind == 1 {
|
||||
fr.left -= 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
t.frames.pop();
|
||||
match t.frames.last_mut() {
|
||||
Some(parent) => parent.idx += 1,
|
||||
None => return true,
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let inner = |branch: usize, left: i64, kind: u8| {
|
||||
let mut p = lp.clone();
|
||||
p.push(idx);
|
||||
p.push(branch);
|
||||
Frame { lp: p, idx: 0, left, loop_kind: kind }
|
||||
};
|
||||
match &list[idx] {
|
||||
Block::If { cond, .. } => {
|
||||
let c = self.cond(s, cond, &t.ev);
|
||||
t.frames.push(inner(if c { 0 } else { 1 }, -1, 0));
|
||||
}
|
||||
Block::Repeat { times, .. } => {
|
||||
let n = self.eval(s, times, &t.ev).round().clamp(0.0, 100.0) as i64;
|
||||
if n > 0 {
|
||||
t.frames.push(inner(0, n - 1, 1));
|
||||
} else {
|
||||
t.frames.last_mut().unwrap().idx += 1;
|
||||
}
|
||||
}
|
||||
Block::Until { cond, .. } => {
|
||||
// checked before each run: already true means the inside never runs
|
||||
if self.cond(s, cond, &t.ev) {
|
||||
t.frames.last_mut().unwrap().idx += 1;
|
||||
} else {
|
||||
t.frames.push(inner(0, -1, 2));
|
||||
}
|
||||
}
|
||||
Block::DoUntil { .. } => t.frames.push(inner(0, -1, 3)),
|
||||
Block::Wait { secs } => {
|
||||
let n = self.eval(s, secs, &t.ev).clamp(0.1, 36000.0) as f32;
|
||||
t.frames.last_mut().unwrap().idx += 1;
|
||||
t.wake = self.time + n;
|
||||
return false;
|
||||
}
|
||||
b => {
|
||||
let b = b.clone();
|
||||
t.frames.last_mut().unwrap().idx += 1;
|
||||
self.exec(s, &b, &t.ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a value
|
||||
fn eval(&self, s: &mut ScriptState, v: &Val, ev: &EventInfo) -> f64 {
|
||||
let r = match v {
|
||||
Val::Num { n } => *n,
|
||||
Val::Bool { b } => *b as i32 as f64,
|
||||
Val::Var { name } => s.vars.get(name).copied().unwrap_or(0.0),
|
||||
Val::Gold => self.gold as f64,
|
||||
Val::Mana => self.mana as f64,
|
||||
Val::Time => self.time as f64,
|
||||
Val::Creatures { side, kind } => self.ents.iter().filter(|e| e.alive && e.captive == 0 && !e.chained && matches(e.side(), e.kind, *side, kind)).count() as f64,
|
||||
Val::InZone { zone, side, kind } => match s.zone(zone) {
|
||||
Some(z) => self.ents.iter().filter(|e| e.alive && !e.held && e.captive == 0 && matches(e.side(), e.kind, *side, kind) && zone_contains(z, e.x, e.y)).count() as f64,
|
||||
None => 0.0,
|
||||
},
|
||||
Val::Random { a, b } => {
|
||||
let (a, b) = (self.eval(s, a, ev), self.eval(s, b, ev));
|
||||
let (lo, hi) = (a.min(b).round(), a.max(b).round());
|
||||
lo + (xorshift(&mut s.rng) as f64 % (hi - lo + 1.0)).floor()
|
||||
}
|
||||
Val::Math { op, a, b } => {
|
||||
let (a, b) = (self.eval(s, a, ev), self.eval(s, b, ev));
|
||||
match op.as_str() {
|
||||
"-" => a - b,
|
||||
"*" => a * b,
|
||||
"/" => {
|
||||
if b == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
a / b
|
||||
}
|
||||
}
|
||||
_ => a + b,
|
||||
}
|
||||
}
|
||||
Val::Cmp { op, a, b } => {
|
||||
let (a, b) = (self.eval(s, a, ev), self.eval(s, b, ev));
|
||||
cmp(op, a, b) as i32 as f64
|
||||
}
|
||||
Val::And { a, b } => (self.cond(s, a, ev) && self.cond(s, b, ev)) as i32 as f64,
|
||||
Val::Or { a, b } => (self.cond(s, a, ev) || self.cond(s, b, ev)) as i32 as f64,
|
||||
Val::Not { a } => (!self.cond(s, a, ev)) as i32 as f64,
|
||||
Val::EventKind { kind } => (ev.kind.is_some() && ev.kind == kind_by_name(kind)) as i32 as f64,
|
||||
};
|
||||
if r.is_finite() {
|
||||
r
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
fn cond(&self, s: &mut ScriptState, v: &Val, ev: &EventInfo) -> bool {
|
||||
self.eval(s, v, ev) != 0.0
|
||||
}
|
||||
|
||||
// one block that does something
|
||||
fn exec(&mut self, s: &mut ScriptState, b: &Block, ev: &EventInfo) {
|
||||
let act = match b {
|
||||
Block::Message { text } => Act::Message(text.clone()),
|
||||
Block::Objective { text } => Act::Objective(text.clone()),
|
||||
Block::Hint { text } => Act::Hint(text.clone()),
|
||||
Block::Spawn { side, kind, count, level, place, name } => {
|
||||
let (pk, pz) = match place {
|
||||
Place::Zone(z) => ("zone", z.clone()),
|
||||
Place::Event => ("event", String::new()),
|
||||
Place::Core => ("core", String::new()),
|
||||
};
|
||||
Act::Spawn { side: *side, kind: kind.clone(), count: self.eval(s, count, ev).round() as i64, level: self.eval(s, level, ev).round() as i64, place: pk.into(), zone: pz, name: name.clone() }
|
||||
}
|
||||
Block::Wave { count } => Act::Wave(self.eval(s, count, ev).round() as i64),
|
||||
Block::Gold { amount } => Act::Gold(self.eval(s, amount, ev)),
|
||||
Block::Mana { amount } => Act::Mana(self.eval(s, amount, ev)),
|
||||
Block::SetVar { name, value } => {
|
||||
let v = self.eval(s, value, ev);
|
||||
s.vars.insert(name.clone(), v);
|
||||
return;
|
||||
}
|
||||
Block::AddVar { name, value } => {
|
||||
let v = s.vars.get(name).copied().unwrap_or(0.0) + self.eval(s, value, ev);
|
||||
s.vars.insert(name.clone(), v);
|
||||
return;
|
||||
}
|
||||
Block::Win => Act::Win,
|
||||
Block::Lose => Act::Lose,
|
||||
Block::Reveal { zone } => Act::Reveal(zone.clone()),
|
||||
Block::Camera { zone } => Act::Camera(zone.clone()),
|
||||
Block::Terrain { zone, kind } => Act::Terrain(zone.clone(), kind.clone()),
|
||||
Block::Kill { zone, side } => Act::Kill(zone.clone(), *side),
|
||||
Block::Trigger { name, on } => Act::Trigger(name.clone(), *on),
|
||||
// the flow blocks are handled by run_thread
|
||||
Block::If { .. } | Block::Repeat { .. } | Block::Until { .. } | Block::DoUntil { .. } | Block::Wait { .. } => return,
|
||||
};
|
||||
self.apply_act(s, act, ev);
|
||||
}
|
||||
|
||||
// what the next check compares against: living units, towers, rooms, research, zone counts
|
||||
fn remember(&self, s: &mut ScriptState, first: bool) {
|
||||
s.alive = self.ents.iter().filter(|e| e.alive && e.captive == 0).map(|e| Seen { id: e.id, kind: e.kind, side: e.side(), x: e.x, y: e.y }).collect();
|
||||
@@ -903,6 +1005,70 @@ mod tests {
|
||||
assert_eq!(v.get("rich"), Some(&1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn waits_and_both_kinds_of_until_loops() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
let cmp = |op: &str, a: Val, b: f64| Val::Cmp { op: op.into(), a: Box::new(a), b: Box::new(Val::num(b)) };
|
||||
let var = |n: &str| Val::Var { name: n.into() };
|
||||
let scripts = MissionScripts {
|
||||
start: vec![
|
||||
Block::SetVar { name: "a".into(), value: Val::num(0.0) },
|
||||
Block::SetVar { name: "pre".into(), value: Val::num(0.0) },
|
||||
Block::SetVar { name: "post".into(), value: Val::num(0.0) },
|
||||
// repeat until (checked before): already true, so the inside never runs
|
||||
Block::Until { cond: cmp(">=", var("a"), 0.0), body: vec![Block::AddVar { name: "pre".into(), value: Val::num(1.0) }] },
|
||||
// repeat ... until (checked after): runs once even though it is already true
|
||||
Block::DoUntil { cond: cmp(">=", var("a"), 0.0), body: vec![Block::AddVar { name: "post".into(), value: Val::num(1.0) }] },
|
||||
// count to three, one step every second
|
||||
Block::Until {
|
||||
cond: cmp(">=", var("a"), 3.0),
|
||||
body: vec![Block::Wait { secs: Val::num(1.0) }, Block::AddVar { name: "a".into(), value: Val::num(1.0) }],
|
||||
},
|
||||
Block::SetVar { name: "done".into(), value: Val::num(1.0) },
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
g.script = Some(ScriptState::new(scripts, vec![], 7));
|
||||
run(&mut g, 0.3);
|
||||
let v = |g: &Game, k: &str| g.script.as_ref().unwrap().vars.get(k).copied();
|
||||
assert_eq!(v(&g, "pre"), Some(0.0), "repeat until checks first");
|
||||
assert_eq!(v(&g, "post"), Some(1.0), "repeat ... until runs once");
|
||||
assert_eq!(v(&g, "a"), Some(0.0), "waiting");
|
||||
assert_eq!(v(&g, "done"), None);
|
||||
run(&mut g, 1.5);
|
||||
assert_eq!(v(&g, "a"), Some(1.0), "one step after a second");
|
||||
run(&mut g, 2.2);
|
||||
assert_eq!(v(&g, "a"), Some(3.0));
|
||||
assert_eq!(v(&g, "done"), Some(1.0), "the script went on after the loop");
|
||||
// a waiting script survives saving and loading
|
||||
let mut g2 = Game::new(3, 64, 64);
|
||||
g2.script = Some(ScriptState::new(
|
||||
MissionScripts { start: vec![Block::Wait { secs: Val::num(2.0) }, Block::SetVar { name: "late".into(), value: Val::num(1.0) }], ..Default::default() },
|
||||
vec![],
|
||||
1,
|
||||
));
|
||||
run(&mut g2, 0.5);
|
||||
let json = serde_json::to_string(&g2).unwrap();
|
||||
let mut g3: Game = serde_json::from_str(&json).unwrap();
|
||||
g3.map.activate();
|
||||
run(&mut g3, 2.0);
|
||||
assert_eq!(g3.script.as_ref().unwrap().vars.get("late"), Some(&1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_endless_loop_does_not_hang_the_game() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
let scripts = MissionScripts {
|
||||
start: vec![Block::Until { cond: Val::Bool { b: false }, body: vec![Block::AddVar { name: "n".into(), value: Val::num(1.0) }] }],
|
||||
..Default::default()
|
||||
};
|
||||
g.script = Some(ScriptState::new(scripts, vec![], 7));
|
||||
run(&mut g, 1.0);
|
||||
let n = g.script.as_ref().unwrap().vars["n"];
|
||||
assert!(n > 100.0, "the loop runs: {}", n);
|
||||
assert!(g.time > 0.9, "and the game goes on");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zone_shapes() {
|
||||
let r = rect("r", (2.0, 2.0), (4.0, 3.0));
|
||||
|
||||
Reference in new issue
Block a user