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
+453
-673
No files matched your search
@@ -69,11 +69,10 @@ But before committing a completed change, the full local checks should pass unle
|
||||
## Campaign scripting conventions
|
||||
|
||||
- Mission scripts are **blocks** (`src/blocks.rs`), edited in the in-game block editor (`src/editor/blocks_ui.rs`); players never see code.
|
||||
- Blocks are translated into Rhai internally and run by `src/mscript.rs` inside the fixed simulation step (deterministic, co-op safe).
|
||||
- The Rust Rhai dependency must keep `no_time` enabled in `Cargo.toml` so the WASM output stays loadable by the macroquad JS bundle.
|
||||
- Rhai reserves `spawn`; the internal API uses `spawn_unit`.
|
||||
- The blocks are run directly by the interpreter in `src/mscript.rs` inside the fixed simulation step (deterministic, co-op safe); there is no scripting language underneath (Rhai was removed when "wait" came).
|
||||
- Script runs are threads (resumable after "wait", saved with the game); loops have a step budget per check so nothing can hang.
|
||||
- Per mission: special scripts (start, victory check, defeat check, clock) and triggers (`TriggerKind` in `src/campaign_doc.rs`).
|
||||
- New block kinds need: the `Block`/`Val` variant, `template`/`kind_name`, palette entry, checking, Rhai translation, the API function in `mscript.rs`, the editor fields in `blocks_ui.rs`, `blk.*` texts in every language file, and a test.
|
||||
- New block kinds need: the `Block`/`Val` variant, `template`/`kind_name`, palette entry, checking (`blocks::check`, `vars_set`), execution in `mscript.rs` (`exec`/`run_thread`/`eval`), nesting in `list_at` (mscript) and `list_mut` (blocks_ui) if it has a body, the editor fields in `blocks_ui.rs`, `blk.*` texts in every language file, and a test.
|
||||
- Side numbers: -1 any, 0 player, 1 heroes, 2 Mossgrim, 3 Lady Nightwell, 4 rival 3.
|
||||
- Unknown creature names, zones, triggers and unset variables are reported by `blocks::check`.
|
||||
- Reference: `docs/CAMPAIGN_EDITOR.md` and `docs/MISSION_SCRIPTING.md`.
|
||||
|
||||
Generated
+2
-319
@@ -8,19 +8,6 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"const-random",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
@@ -39,18 +26,6 @@ version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.2"
|
||||
@@ -63,15 +38,6 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
@@ -84,39 +50,6 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79fcda08c33bb58b97008b2cdada6622500e949e060f5913361763121abd2416"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"static_assertions",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core_maths"
|
||||
version = "0.1.1"
|
||||
@@ -135,18 +68,11 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "dungeoncraft"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"macroquad",
|
||||
"rhai",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@@ -193,41 +119,6 @@ dependencies = [
|
||||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.27.0"
|
||||
@@ -264,17 +155,6 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.191"
|
||||
@@ -378,40 +258,19 @@ dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
@@ -436,41 +295,6 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai"
|
||||
version = "1.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3925b784c0bee6542637e425ed7511a63257bf11b7ecf51ffd845e2d24d9163"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bitflags 2.13.2",
|
||||
"compact_str",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rhai_codegen",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
"thin-vec",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_codegen"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d9661edb4a4b8145b017582d634c5eb88ff1a5284a5aa701f83a28f0657a826"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
@@ -498,7 +322,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -520,46 +344,6 @@ version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8"
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.9"
|
||||
@@ -571,21 +355,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6a4b9ba8738cb4a4f399d37e266becfd475e75eb73425b87a05a2f2039ba63e"
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.25.1"
|
||||
@@ -601,72 +370,6 @@ version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -689,26 +392,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.8"
|
||||
|
||||
@@ -6,7 +6,6 @@ license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
macroquad = { version = "0.4" }
|
||||
rhai = { version = "1.27.0", default-features = false, features = ["std", "no_time"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
|
||||
+18
-22
@@ -1,12 +1,12 @@
|
||||
# Mission scripting with blocks
|
||||
|
||||
Missions of editor campaigns are scripted with **blocks** (like Blockly): coloured puzzle pieces
|
||||
put together in the block editor (see [CAMPAIGN_EDITOR.md](CAMPAIGN_EDITOR.md)). Players never see
|
||||
code – internally the blocks are translated into Rhai, compiled once per mission and run inside the
|
||||
game's fixed simulation step, so a co-op match runs them identically on every computer.
|
||||
put together in the block editor (see [CAMPAIGN_EDITOR.md](CAMPAIGN_EDITOR.md)). There is no code:
|
||||
the game runs the blocks directly, inside its fixed simulation step, so a co-op match runs them
|
||||
identically on every computer.
|
||||
|
||||
Code: `src/blocks.rs` (blocks, values, checking, translation), `src/mscript.rs` (run time, triggers,
|
||||
zones), `src/campaign_doc.rs` (`MissionScripts`, `TriggerDoc`, `TriggerKind`).
|
||||
Code: `src/blocks.rs` (blocks, values, checking), `src/mscript.rs` (run time, triggers, zones),
|
||||
`src/campaign_doc.rs` (`MissionScripts`, `TriggerDoc`, `TriggerKind`).
|
||||
|
||||
## Scripts of a mission
|
||||
|
||||
@@ -64,6 +64,9 @@ creatures and heroes.
|
||||
| | Gold + / Mana + | amount (negative takes) |
|
||||
| Logic | If / else | condition; blocks inside |
|
||||
| | Repeat | times (0–100); blocks inside |
|
||||
| | Repeat until | condition, checked **before** each run (already true: the inside never runs); blocks inside |
|
||||
| | Repeat, then check: until | condition, checked **after** each run (the inside runs at least once); blocks inside |
|
||||
| | Wait | seconds – the script pauses here and goes on with the next block later (also inside loops and "if") |
|
||||
| | Switch trigger | trigger, on/off |
|
||||
| Variables | Set | variable, value |
|
||||
| | Change | variable, value to add |
|
||||
@@ -91,28 +94,21 @@ campaign file check (`CampaignDoc::validate`).
|
||||
|
||||
## At run time (`src/mscript.rs`)
|
||||
|
||||
- The mission's program is generated by `mscript::program` – one Rhai function per script and
|
||||
trigger (`s_start`, `s_victory`, `s_defeat`, `s_tick`, `t_0`, `t_1`, ...) – and compiled once.
|
||||
- Scripts read the game (`m.gold()`, `m.count(side, kind)`, `m.in_zone(zone, side, kind)`, ...)
|
||||
and collect actions (`m.message`, `m.spawn_unit`, `m.terrain`, `m.win`, ...), which are applied
|
||||
after the call. Variables, the random state, fired triggers and zone contents are part of
|
||||
`ScriptState` and saved with the game.
|
||||
- Every run of a script (the start, each second's checks, each fired trigger) is a **thread**: the
|
||||
lists of blocks it is in and where. *Wait* parks a thread until its time; threads are saved with
|
||||
the game, so a waiting script goes on after loading.
|
||||
- The victory check, defeat check and clock start every second, but not while their previous run is
|
||||
still waiting. A trigger that fires again while its last run waits starts a second run.
|
||||
- Loops without a *Wait* run up to 2000 blocks per check (ten checks a second) and then go on at the
|
||||
next check – a script can never hang the game.
|
||||
- Blocks act at once, in order: a *Count* after a *Spawn* already sees the new units. Variables, the
|
||||
random state, fired triggers, zone contents and waiting threads are part of `ScriptState`.
|
||||
- Scripts always run from the first player's side; messages reach every player of a match.
|
||||
- A script error stops nothing else: it is shown once as a message and written to the test log.
|
||||
- Test play shows the variables and the trigger log at the top right, and the zones on the map.
|
||||
|
||||
### Rhai API (internal)
|
||||
|
||||
`gold() mana() time() get(name) set(name, v) count(side, kind) in_zone(zone, side, kind)
|
||||
random(a, b) div(a, b) times(n) event_is(kind) message(t) objective(t) hint(t)
|
||||
spawn_unit(side, kind, count, level, place, zone, name) wave(n) add_gold(n) add_mana(n) win() lose()
|
||||
reveal(zone) camera(zone) terrain(zone, kind) kill(zone, side) trigger(name, on)` – numbers are
|
||||
floats; `spawn` is a reserved word in Rhai, hence `spawn_unit`. The Rhai dependency keeps the
|
||||
`no_time` feature (WASM).
|
||||
|
||||
## The demo
|
||||
|
||||
*Open the demo* in the campaign editor: mission 1 ("The Trial of Triggers") uses all 14 trigger
|
||||
kinds, all special scripts and nearly every block on a painted map with five zones (rectangle,
|
||||
kinds, all special scripts and every block (including both kinds of *until* loops and *Wait*) on a painted map with five zones (rectangle,
|
||||
circle, line, tiles); mission 2 is a random map behind a branch. Play it with *Test play* and watch
|
||||
the debug view.
|
||||
@@ -1331,3 +1331,10 @@ demo.m2.title = Neu gewürfelt
|
||||
demo.m2.story = Diese Karte wird für jeden Durchgang der Kampagne neu gewürfelt. Besiege alle Helden.
|
||||
demo.m2.start = Eine Zufallskarte. Selbst ich habe sie noch nie gesehen.
|
||||
demo.m2.wave = Zwei Minuten sind um: Die Helden kommen.
|
||||
blk.until = Wiederhole bis
|
||||
blk.dountil = Wiederhole, prüfe danach: bis
|
||||
blk.wait = Warte
|
||||
blk.w_seconds = Sekunden
|
||||
blk.help.until = Führt die Blöcke darin immer wieder aus, bis die Bedingung gilt - geprüft vor jedem Durchlauf; gilt sie schon, läuft der Inhalt gar nicht.
|
||||
blk.help.dountil = Führt die Blöcke darin aus, prüft dann die Bedingung und wiederholt, bis sie gilt - der Inhalt läuft mindestens einmal.
|
||||
blk.help.wait = Hält dieses Skript einige Sekunden an, danach geht es mit dem nächsten Block weiter (auch in Schleifen).
|
||||
@@ -1332,3 +1332,10 @@ demo.m2.title = Rolled Anew
|
||||
demo.m2.story = This map is rolled anew for every run of the campaign. Defeat every hero.
|
||||
demo.m2.start = A random map. Even I have never seen it before.
|
||||
demo.m2.wave = Two minutes are over: the heroes come.
|
||||
blk.until = Repeat until
|
||||
blk.dountil = Repeat, then check: until
|
||||
blk.wait = Wait
|
||||
blk.w_seconds = seconds
|
||||
blk.help.until = Runs the blocks inside again and again until the condition holds - checked before each run, so if it already holds the inside does not run at all.
|
||||
blk.help.dountil = Runs the blocks inside, then checks the condition and repeats until it holds - the inside always runs at least once.
|
||||
blk.help.wait = Pauses this script for some seconds, then it goes on with the next block (also inside loops).
|
||||
@@ -1332,3 +1332,10 @@ net.coop_editor = Editor campaign
|
||||
net.coop_builtin = Built-in campaign
|
||||
net.coop_editor_mission = Editor campaign {0}: mission {1} ({2})
|
||||
net.coop_editor_none = No mission of the editor campaign fits co-op (map at least 72x72, a goal besides rival masters)
|
||||
blk.until = Repetir hasta
|
||||
blk.dountil = Repetir y luego: hasta
|
||||
blk.wait = Esperar
|
||||
blk.w_seconds = segundos
|
||||
blk.help.until = Ejecuta los bloques de dentro una y otra vez hasta que se cumpla la condición; se comprueba antes de cada vuelta, así que si ya se cumple no se ejecuta nada.
|
||||
blk.help.dountil = Ejecuta los bloques de dentro, luego comprueba la condición y repite hasta que se cumpla; el contenido se ejecuta al menos una vez.
|
||||
blk.help.wait = Detiene este script unos segundos y luego sigue con el siguiente bloque (también dentro de bucles).
|
||||
@@ -1332,3 +1332,10 @@ net.coop_editor = Editor campaign
|
||||
net.coop_builtin = Built-in campaign
|
||||
net.coop_editor_mission = Editor campaign {0}: mission {1} ({2})
|
||||
net.coop_editor_none = No mission of the editor campaign fits co-op (map at least 72x72, a goal besides rival masters)
|
||||
blk.until = Ripeti finché
|
||||
blk.dountil = Ripeti, poi controlla: finché
|
||||
blk.wait = Attendi
|
||||
blk.w_seconds = secondi
|
||||
blk.help.until = Esegue i blocchi interni più volte finché la condizione è vera; viene controllata prima di ogni giro, quindi se è già vera il contenuto non viene eseguito.
|
||||
blk.help.dountil = Esegue i blocchi interni, poi controlla la condizione e ripete finché è vera; il contenuto viene eseguito almeno una volta.
|
||||
blk.help.wait = Mette in pausa questo script per alcuni secondi, poi prosegue con il blocco successivo (anche nei cicli).
|
||||
@@ -1332,3 +1332,10 @@ net.coop_editor = Editor campaign
|
||||
net.coop_builtin = Built-in campaign
|
||||
net.coop_editor_mission = Editor campaign {0}: mission {1} ({2})
|
||||
net.coop_editor_none = No mission of the editor campaign fits co-op (map at least 72x72, a goal besides rival masters)
|
||||
blk.until = Повторять, пока не
|
||||
blk.dountil = Повторять, затем проверить: пока не
|
||||
blk.wait = Ждать
|
||||
blk.w_seconds = сек.
|
||||
blk.help.until = Снова и снова выполняет блоки внутри, пока условие не станет верным; проверка перед каждым проходом, поэтому если оно уже верно, блоки не выполняются.
|
||||
blk.help.dountil = Выполняет блоки внутри, затем проверяет условие и повторяет, пока оно не станет верным; блоки выполняются хотя бы один раз.
|
||||
blk.help.wait = Приостанавливает этот скрипт на несколько секунд, затем выполняется следующий блок (и внутри циклов).
|
||||
+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);
|
||||
|
||||
|
||||
+340
-174
@@ -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));
|
||||
// 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
|
||||
}
|
||||
code
|
||||
}
|
||||
Block::Repeat { body, .. } | Block::Until { body, .. } | Block::DoUntil { body, .. } => body,
|
||||
_ => return None,
|
||||
};
|
||||
k += 2;
|
||||
}
|
||||
Some(cur)
|
||||
}
|
||||
|
||||
#[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);
|
||||
fn xorshift(rng: &mut u32) -> u32 {
|
||||
let mut x = (*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()),
|
||||
}
|
||||
})
|
||||
})
|
||||
*rng = x;
|
||||
x
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ the game side
|
||||
@@ -429,24 +349,27 @@ 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 {
|
||||
}
|
||||
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;
|
||||
@@ -454,33 +377,212 @@ impl Game {
|
||||
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 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
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